From 8fa1263bc657b7d1d0630bc193097cb5d4aa631a Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 18 Aug 2021 15:54:10 -0500 Subject: [PATCH] feat(app-distribution): Implement Firebase App Distribution module --- .spellcheck.dict.txt | 6 + docs/app-check/usage/index.md | 2 +- docs/app-distribution/usage/index.md | 58 +++ docs/auth/usage/index.md | 2 +- docs/sidebar.yaml | 4 + packages/app-distribution/.npmignore | 65 ++++ packages/app-distribution/LICENSE | 32 ++ packages/app-distribution/README.md | 54 +++ .../RNFBAppDistribution.podspec | 45 +++ .../__tests__/app-distribution.test.ts | 11 + .../app-distribution/android/.editorconfig | 10 + .../app-distribution/android/build.gradle | 98 +++++ packages/app-distribution/android/lint.xml | 5 + .../app-distribution/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 8 + ...ctNativeFirebaseAppDistributionModule.java | 29 ++ ...tNativeFirebaseAppDistributionPackage.java | 42 +++ .../e2e/app-distribution.e2e.js | 58 +++ .../project.pbxproj | 349 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + .../xcshareddata/IDETemplateMacros.plist | 24 ++ .../RNFBAppDistributionModule.h | 24 ++ .../RNFBAppDistributionModule.m | 108 ++++++ packages/app-distribution/lib/index.d.ts | 164 ++++++++ packages/app-distribution/lib/index.js | 86 +++++ packages/app-distribution/package.json | 30 ++ packages/app-distribution/type-test.ts | 26 ++ packages/app/lib/internal/constants.js | 1 + tests/app.js | 1 + tests/e2e/.mocharc.js | 1 + tests/ios/Podfile.lock | 19 + tests/package.json | 1 + website/scripts/source-reference.js | 2 + website/src/templates/utils.ts | 2 + .../app-distribution/app-distribution.png | Bin 0 -> 11644 bytes 37 files changed, 1386 insertions(+), 2 deletions(-) create mode 100644 docs/app-distribution/usage/index.md create mode 100644 packages/app-distribution/.npmignore create mode 100644 packages/app-distribution/LICENSE create mode 100644 packages/app-distribution/README.md create mode 100644 packages/app-distribution/RNFBAppDistribution.podspec create mode 100644 packages/app-distribution/__tests__/app-distribution.test.ts create mode 100644 packages/app-distribution/android/.editorconfig create mode 100644 packages/app-distribution/android/build.gradle create mode 100644 packages/app-distribution/android/lint.xml create mode 100644 packages/app-distribution/android/settings.gradle create mode 100644 packages/app-distribution/android/src/main/AndroidManifest.xml create mode 100644 packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionModule.java create mode 100644 packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionPackage.java create mode 100644 packages/app-distribution/e2e/app-distribution.e2e.js create mode 100644 packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.pbxproj create mode 100644 packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/xcshareddata/IDETemplateMacros.plist create mode 100644 packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.h create mode 100644 packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m create mode 100644 packages/app-distribution/lib/index.d.ts create mode 100644 packages/app-distribution/lib/index.js create mode 100644 packages/app-distribution/package.json create mode 100644 packages/app-distribution/type-test.ts create mode 100644 website/static/assets/docs/app-distribution/app-distribution.png diff --git a/.spellcheck.dict.txt b/.spellcheck.dict.txt index d7d2f40149..21a2d9322b 100644 --- a/.spellcheck.dict.txt +++ b/.spellcheck.dict.txt @@ -31,6 +31,7 @@ barcodes BUGFIX CarPlay Changelog +checkForUpdate CLI CocoaPods codebase @@ -44,6 +45,7 @@ Deprecations Detox DEVEX Diarmid +Distribution DeviceCheck dropdown e2e @@ -70,6 +72,7 @@ HTTP HTTPS IDFA installable +integrations IntelliSense Interstitials interstitials @@ -78,6 +81,7 @@ invertase iOS iOS13 IPs +isTesterSignedIn Javascript javascript JS @@ -111,6 +115,7 @@ personalization Podfile plist pre-fetched +pre-release pre-rendered preflight preloaded @@ -141,6 +146,7 @@ setBackgroundMessageHandler SHA1 SHA-256 SIGABRT +signInTester Siri SMS src diff --git a/docs/app-check/usage/index.md b/docs/app-check/usage/index.md index 9730c9e146..e7601943eb 100644 --- a/docs/app-check/usage/index.md +++ b/docs/app-check/usage/index.md @@ -2,7 +2,7 @@ title: App Check description: Installation and getting started with App Check. icon: //static.invertase.io/assets/social/firebase-logo.png -next: /auth/usage +next: /app-distribution/usage previous: /analytics/screen-tracking --- diff --git a/docs/app-distribution/usage/index.md b/docs/app-distribution/usage/index.md new file mode 100644 index 0000000000..0cabc293b9 --- /dev/null +++ b/docs/app-distribution/usage/index.md @@ -0,0 +1,58 @@ +--- +title: App Distribution +description: Installation and getting started with App Distribution. +icon: /assets/docs/app-distribution/app-distribution.png +next: /auth/usage +previous: /app-check/usage +--- + +# Installation + +This module requires that the `@react-native-firebase/app` module is already setup and installed. To install the "app" +module, view the [Getting Started](/) documentation. + +```bash +# Install & setup the app module +yarn add @react-native-firebase/app + +# Install the app-check module +yarn add @react-native-firebase/app-distribution + +# If you're developing your app using iOS, run this command +cd ios/ && pod install +``` + +# What does it do + +Firebase App Distribution gives a holistic view of your beta testing program across iOS and Android, providing you with valuable feedback before a new release is in production. You can send pre-release versions of your app using the console or your CI servers, and installing your app is easy for testers. + +Firebase App Distribution makes distributing your apps to trusted testers painless. By getting your apps onto testers' devices quickly, you can get feedback early and often. And if you use Crashlytics in your apps, you’ll automatically get stability metrics for all your builds, so you know when you’re ready to ship. + + + +# Key capabilities + +- Cross-platform Manage both your iOS and Android pre-release distributions from the same place. +- Fast distributions Get early releases into your testers' hands quickly, with fast onboarding, no SDK to install, and instant app delivery. +- Fits into your workflow Distribute builds using the Firebase console, the Firebase Command Line Interface (CLI) tool, - or Gradle (Android). Automate distribution by integrating the CLI into CI jobs. +- Tester management Manage your testing teams by organizing them into groups. Easily add new testers with email invitations that walk them through the onboarding process. See the status of each tester for specific versions of your app: view who has accepted a testing invitation and downloaded the app. +- Works with Android App Bundles Distribute releases to testers for your Android App Bundle in Google Play. App - Distribution integrates with Google Play's internal app sharing service to streamline your app testing and launching processes. +- Works with Crashlytics When combined with Crashlytics, get insights into the stability of your test distributions. + +The [official Firebase App Check documentation](https://firebase.google.com/docs/app-distribution) has more information, including about build upload integration, it is worth a read. + +# Usage + +The react-native-firebase module for App Distribution is meant to expose the new version alert capabilities of the iOS SDK. The majority of the App Distribution Firebase service depends on native build/release integrations. Those build/release integrations must be natively implemented for iOS and Android, according to [the upstream docs from Firebase](https://firebase.google.com/docs/app-distribution) or our build system provider. + +## New Version Alerts + +On iOS if you include the App Distribution module, you can optionally enable in-app alerts that appear when new builds are available to test. + +## Tester Sign-in Status + +The methods signInTester and isTesterSignedIn give you more flexibility customizing your tester's sign-in experience, so it can better match your app's look and feel. + +You may check if your tester has already signed into their Firebase App Distribution tester account, so you can choose to display your sign-in UI only for testers who haven't yet signed in. After the tester has signed in, you can then call checkForUpdate to check whether the tester has access to a new build. + +## diff --git a/docs/auth/usage/index.md b/docs/auth/usage/index.md index b5a15f3513..b631167cd2 100644 --- a/docs/auth/usage/index.md +++ b/docs/auth/usage/index.md @@ -3,7 +3,7 @@ title: Authentication description: Installation and getting started with Authentication. icon: //static.invertase.io/assets/firebase/authentication.svg next: /auth/social-auth -previous: /app-check/usage +previous: /app-distribution/usage --- # Installation diff --git a/docs/sidebar.yaml b/docs/sidebar.yaml index 86dd79753a..20b2ebdcc8 100644 --- a/docs/sidebar.yaml +++ b/docs/sidebar.yaml @@ -27,6 +27,10 @@ - - - Usage - '/app-check/usage' - '//static.invertase.io/assets/social/firebase-logo.png' +- - App Distribution + - - - Usage + - '/app-distribution/usage' + - '/assets/docs/app-distribution/app-distribution.png' - - Authentication - - - Usage - '/auth/usage' diff --git a/packages/app-distribution/.npmignore b/packages/app-distribution/.npmignore new file mode 100644 index 0000000000..d9fa30e5a5 --- /dev/null +++ b/packages/app-distribution/.npmignore @@ -0,0 +1,65 @@ +# Built application files +android/*/build/ + +# Crashlytics configuations +android/com_crashlytics_export_strings.xml + +# Local configuration file (sdk path, etc) +android/local.properties + +# Gradle generated files +android/.gradle/ + +# Signing files +android/.signing/ + +# User-specific configurations +android/.idea/gradle.xml +android/.idea/libraries/ +android/.idea/workspace.xml +android/.idea/tasks.xml +android/.idea/.name +android/.idea/compiler.xml +android/.idea/copyright/profiles_settings.xml +android/.idea/encodings.xml +android/.idea/misc.xml +android/.idea/modules.xml +android/.idea/scopes/scope_settings.xml +android/.idea/vcs.xml +android/*.iml + +# Xcode +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +*.xcuserstate +ios/Pods +ios/build +*project.xcworkspace* +*xcuserdata* + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.dbandroid/gradle +android/gradlew +android/build +android/gradlew.bat +android/gradle/ + +.idea +coverage +yarn.lock +e2e/ +.github +.vscode +.nyc_output +android/.settings +*.coverage.json +.circleci +.eslintignore diff --git a/packages/app-distribution/LICENSE b/packages/app-distribution/LICENSE new file mode 100644 index 0000000000..ef3ed44f06 --- /dev/null +++ b/packages/app-distribution/LICENSE @@ -0,0 +1,32 @@ +Apache-2.0 License +------------------ + +Copyright (c) 2016-present Invertase Limited & Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this library except in compliance with the License. + +You may obtain a copy of the Apache-2.0 License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Creative Commons Attribution 3.0 License +---------------------------------------- + +Copyright (c) 2016-present Invertase Limited & Contributors + +Documentation and other instructional materials provided for this project +(including on a separate documentation repository or it's documentation website) are +licensed under the Creative Commons Attribution 3.0 License. Code samples/blocks +contained therein are licensed under the Apache License, Version 2.0 (the "License"), as above. + +You may obtain a copy of the Creative Commons Attribution 3.0 License at + + https://creativecommons.org/licenses/by/3.0/ diff --git a/packages/app-distribution/README.md b/packages/app-distribution/README.md new file mode 100644 index 0000000000..6271246579 --- /dev/null +++ b/packages/app-distribution/README.md @@ -0,0 +1,54 @@ +

+ +
+
+

React Native Firebase - App Distribution

+

+ +

+ Coverage + NPM downloads + NPM version + License + Maintained with Lerna +

+ +

+ Chat on Discord + Follow on Twitter + Follow on Facebook +

+ +--- + +App Distribution helps you distribute your pre-release app versions to users + +[> Learn More](https://firebase.google.com/products/app-distribution/) + +## Installation + +Requires `@react-native-firebase/app` to be installed. + +```bash +yarn add @react-native-firebase/app-distribution +``` + +## Documentation + +- [Guides](https://rnfirebase.io/app-distribution/usage) +- [Reference](https://rnfirebase.io/reference/app-distribution) + +## License + +- See [LICENSE](/LICENSE) + +--- + +

+ +

+ Built and maintained with 💛 by Invertase. +

+

+ +--- diff --git a/packages/app-distribution/RNFBAppDistribution.podspec b/packages/app-distribution/RNFBAppDistribution.podspec new file mode 100644 index 0000000000..e36e1cebd9 --- /dev/null +++ b/packages/app-distribution/RNFBAppDistribution.podspec @@ -0,0 +1,45 @@ +require 'json' +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) +appPackage = JSON.parse(File.read(File.join('..', 'app', 'package.json'))) + +coreVersionDetected = appPackage['version'] +coreVersionRequired = package['peerDependencies'][appPackage['name']] +firebase_sdk_version = appPackage['sdkVersions']['ios']['firebase'] +if coreVersionDetected != coreVersionRequired + Pod::UI.warn "NPM package '#{package['name']}' depends on '#{appPackage['name']}' v#{coreVersionRequired} but found v#{coreVersionDetected}, this might cause build issues or runtime crashes." +end + +Pod::Spec.new do |s| + s.name = "RNFBAppDistribution" + s.version = package["version"] + s.description = package["description"] + s.summary = <<-DESC + A well tested feature rich Firebase implementation for React Native, supporting iOS & Android. + DESC + s.homepage = "http://invertase.io/oss/react-native-firebase" + s.license = package['license'] + s.authors = "Invertase Limited" + s.source = { :git => "https://github.com/invertase/react-native-firebase.git", :tag => "v#{s.version}" } + s.social_media_url = 'http://twitter.com/invertaseio' + s.ios.deployment_target = "10.0" + s.source_files = 'ios/**/*.{h,m}' + + # React Native dependencies + s.dependency 'React-Core' + s.dependency 'RNFBApp' + + if defined?($FirebaseSDKVersion) + Pod::UI.puts "#{s.name}: Using user specified Firebase SDK version '#{$FirebaseSDKVersion}'" + firebase_sdk_version = $FirebaseSDKVersion + end + + # Firebase dependencies + s.dependency 'Firebase/AppDistribution', firebase_sdk_version + + if defined?($RNFirebaseAsStaticFramework) + Pod::UI.puts "#{s.name}: Using overridden static_framework value of '#{$RNFirebaseAsStaticFramework}'" + s.static_framework = $RNFirebaseAsStaticFramework + else + s.static_framework = false + end +end diff --git a/packages/app-distribution/__tests__/app-distribution.test.ts b/packages/app-distribution/__tests__/app-distribution.test.ts new file mode 100644 index 0000000000..f3fe325a72 --- /dev/null +++ b/packages/app-distribution/__tests__/app-distribution.test.ts @@ -0,0 +1,11 @@ +import { firebase } from '../lib'; + +describe('appDistribution()', function () { + describe('namespace', function () { + it('accessible from firebase.app()', function () { + const app = firebase.app(); + expect(app.appDistribution).toBeDefined(); + expect(app.appDistribution().app).toEqual(app); + }); + }); +}); diff --git a/packages/app-distribution/android/.editorconfig b/packages/app-distribution/android/.editorconfig new file mode 100644 index 0000000000..670398e990 --- /dev/null +++ b/packages/app-distribution/android/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/packages/app-distribution/android/build.gradle b/packages/app-distribution/android/build.gradle new file mode 100644 index 0000000000..9b5e62e6ed --- /dev/null +++ b/packages/app-distribution/android/build.gradle @@ -0,0 +1,98 @@ +import io.invertase.gradle.common.PackageJson + +buildscript { + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + if (project == rootProject) { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:7.0.0") + } + } +} + +plugins { + id "io.invertase.gradle.build" version "1.5" +} + +def appProject +if (findProject(':@react-native-firebase_app')) { + appProject = project(':@react-native-firebase_app') +} else if (findProject(':react-native-firebase_app')) { + appProject = project(':react-native-firebase_app') +} else { + throw new GradleException('Could not find the react-native-firebase/app package, have you installed it?') +} +def packageJson = PackageJson.getForProject(project) +def appPackageJson = PackageJson.getForProject(appProject) +def firebaseBomVersion = appPackageJson['sdkVersions']['android']['firebase'] +def jsonMinSdk = appPackageJson['sdkVersions']['android']['minSdk'] +def jsonTargetSdk = appPackageJson['sdkVersions']['android']['targetSdk'] +def jsonCompileSdk = appPackageJson['sdkVersions']['android']['compileSdk'] +def jsonBuildTools = appPackageJson['sdkVersions']['android']['buildTools'] +def coreVersionDetected = appPackageJson['version'] +def coreVersionRequired = packageJson['peerDependencies'][appPackageJson['name']] +// Only log after build completed so log warning appears at the end +if (coreVersionDetected != coreVersionRequired) { + gradle.buildFinished { + project.logger.warn("ReactNativeFirebase WARNING: NPM package '${packageJson['name']}' depends on '${appPackageJson['name']}' v${coreVersionRequired} but found v${coreVersionDetected}, this might cause build issues or runtime crashes.") + } +} + +project.ext { + set('react-native', [ + versions: [ + android : [ + minSdk : jsonMinSdk, + targetSdk : jsonTargetSdk, + compileSdk: jsonCompileSdk, + // optional as gradle.buildTools comes with one by default + // overriding here though to match the version RN uses + buildTools: jsonBuildTools + ], + + firebase: [ + bom: firebaseBomVersion, + ], + ], + ]) +} + +android { + defaultConfig { + multiDexEnabled true + } + lintOptions { + disable 'GradleCompatible' + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + sourceSets { + main { + java.srcDirs = ['src/main/java', 'src/reactnative/java'] + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + api appProject + implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}") +} + +ReactNative.shared.applyPackageVersion() +ReactNative.shared.applyDefaultExcludes() +ReactNative.module.applyAndroidVersions() +ReactNative.module.applyReactNativeDependency("api") diff --git a/packages/app-distribution/android/lint.xml b/packages/app-distribution/android/lint.xml new file mode 100644 index 0000000000..c3dd72aca0 --- /dev/null +++ b/packages/app-distribution/android/lint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app-distribution/android/settings.gradle b/packages/app-distribution/android/settings.gradle new file mode 100644 index 0000000000..db119e8fc8 --- /dev/null +++ b/packages/app-distribution/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = '@react-native-firebase_appdistribution' diff --git a/packages/app-distribution/android/src/main/AndroidManifest.xml b/packages/app-distribution/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..fd0fb7d701 --- /dev/null +++ b/packages/app-distribution/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionModule.java b/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionModule.java new file mode 100644 index 0000000000..822cded2c3 --- /dev/null +++ b/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionModule.java @@ -0,0 +1,29 @@ +package io.invertase.firebase.appdistribution; + +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import com.facebook.react.bridge.*; +import io.invertase.firebase.common.ReactNativeFirebaseModule; + +public class ReactNativeFirebaseAppDistributionModule extends ReactNativeFirebaseModule { + private static final String TAG = "AppDistribution"; + + ReactNativeFirebaseAppDistributionModule(ReactApplicationContext reactContext) { + super(reactContext, TAG); + } +} diff --git a/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionPackage.java b/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionPackage.java new file mode 100644 index 0000000000..03cd077546 --- /dev/null +++ b/packages/app-distribution/android/src/main/java/io/invertase/firebase/appdistribution/ReactNativeFirebaseAppDistributionPackage.java @@ -0,0 +1,42 @@ +package io.invertase.firebase.appdistribution; + +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unused") +public class ReactNativeFirebaseAppDistributionPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new ReactNativeFirebaseAppDistributionModule(reactContext)); + return modules; + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/packages/app-distribution/e2e/app-distribution.e2e.js b/packages/app-distribution/e2e/app-distribution.e2e.js new file mode 100644 index 0000000000..7b89dfdbbb --- /dev/null +++ b/packages/app-distribution/e2e/app-distribution.e2e.js @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +describe('appDistribution()', function () { + describe('isTesterSignedIn()', function () { + it('checks if a tester is signed in', async function () { + if (device.getPlatform() === 'ios') { + await firebase.appDistribution().isTesterSignedIn(); + } else { + this.skip(); + } + }); + }); + // Requires a valid google account logged in on device and associated with iOS testing app + xdescribe('signInTester()', function () { + it('signs a tester in', async function () { + if (device.getPlatform() === 'ios') { + await firebase.appDistribution().signInTester(); + } else { + this.skip(); + } + }); + }); + describe('signOutTester()', function () { + it('signs out a tester', async function () { + if (device.getPlatform() === 'ios') { + await firebase.appDistribution().signOutTester(); + } else { + this.skip(); + } + }); + }); + // Requires a valid google account logged in on device and associated with iOS testing app + // plus a new IPA file uploaded + xdescribe('checkForUpdate()', function () { + it('checks for an update', async function () { + if (device.getPlatform() === 'ios') { + await firebase.appDistribution().checkForUpdate(); + } else { + this.skip(); + } + }); + }); +}); diff --git a/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.pbxproj b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..36828a7107 --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.pbxproj @@ -0,0 +1,349 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 2744B98621F45429004F8E3F /* RNFBAppDistributionModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 2744B98521F45429004F8E3F /* RNFBAppDistributionModule.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2744B98021F45429004F8E3F /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2744B98221F45429004F8E3F /* libRNFBAppDistribution.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFBAppDistribution.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 2744B98421F45429004F8E3F /* RNFBAppDistributionModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNFBAppDistributionModule.h; path = RNFBAppDistribution/RNFBAppDistributionModule.h; sourceTree = SOURCE_ROOT; }; + 2744B98521F45429004F8E3F /* RNFBAppDistributionModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNFBAppDistributionModule.m; path = RNFBAppDistribution/RNFBAppDistributionModule.m; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2744B97F21F45429004F8E3F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2744B97521F452B8004F8E3F /* Products */ = { + isa = PBXGroup; + children = ( + 2744B98221F45429004F8E3F /* libRNFBAppDistribution.a */, + ); + name = Products; + sourceTree = ""; + }; + 2744B98321F45429004F8E3F /* RNFBAppDistribution */ = { + isa = PBXGroup; + children = ( + 2744B9A121F48736004F8E3F /* converters */, + 2744B98C21F45C64004F8E3F /* common */, + 2744B98421F45429004F8E3F /* RNFBAppDistributionModule.h */, + 2744B98521F45429004F8E3F /* RNFBAppDistributionModule.m */, + ); + path = RNFBAppDistribution; + sourceTree = ""; + }; + 3323F52AAFE26B7384BE4DE3 = { + isa = PBXGroup; + children = ( + 2744B98321F45429004F8E3F /* RNFBAppDistribution */, + 2744B97521F452B8004F8E3F /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2744B98121F45429004F8E3F /* RNFBAppDistribution */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2744B98821F45429004F8E3F /* Build configuration list for PBXNativeTarget "RNFBAppDistribution" */; + buildPhases = ( + 2744B97E21F45429004F8E3F /* Sources */, + 2744B97F21F45429004F8E3F /* Frameworks */, + 2744B98021F45429004F8E3F /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RNFBAppDistribution; + productName = RNFBAppDistribution; + productReference = 2744B98221F45429004F8E3F /* libRNFBAppDistribution.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3323F95273A95DB34F55C6D7 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = RNFBAppDistribution; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = Invertase; + TargetAttributes = { + 2744B98121F45429004F8E3F = { + CreatedOnToolsVersion = 10.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 3323F1C5716BA966BBBB95A4 /* Build configuration list for PBXProject "RNFBAppDistribution" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 3323F52AAFE26B7384BE4DE3; + productRefGroup = 2744B97521F452B8004F8E3F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2744B98121F45429004F8E3F /* RNFBAppDistribution */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 2744B97E21F45429004F8E3F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2744B98621F45429004F8E3F /* RNFBAppDistributionModule.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 2744B98921F45429004F8E3F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2744B98A21F45429004F8E3F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3323F77D701E1896E6D239CF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "${BUILT_PRODUCTS_DIR}/**", + "${SRCROOT}/../../../ios/Firebase/**", + "$(FIREBASE_SEARCH_PATH)/Firebase/**", + "$(SRCROOT)/../../../ios/Pods/FirebaseApp-distribution/Frameworks", + "$(SRCROOT)/../../../tests/ios/Pods/FirebaseApp-distribution/Frameworks", + ); + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(REACT_SEARCH_PATH)/React/**", + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../../react-native-firebase/ios/**", + "$(FIREBASE_SEARCH_PATH)/Firebase/**", + "${SRCROOT}/../../../ios/Firebase/**", + "${SRCROOT}/../../../ios/Pods/Headers/Public/**", + "${SRCROOT}/../../../tests/ios/Pods/Headers/Public/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native-firebase/ios/**", + "$(SRCROOT)/../../../packages/app/ios/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 3323F7E33E1559A2B9826720 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "${BUILT_PRODUCTS_DIR}/**", + "${SRCROOT}/../../../ios/Firebase/**", + "$(FIREBASE_SEARCH_PATH)/Firebase/**", + "$(SRCROOT)/../../../ios/Pods/FirebaseApp-distribution/Frameworks", + ); + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(REACT_SEARCH_PATH)/React/**", + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../../react-native-firebase/ios/**", + "$(FIREBASE_SEARCH_PATH)/Firebase/**", + "${SRCROOT}/../../../ios/Firebase/**", + "${SRCROOT}/../../../ios/Pods/Headers/Public/**", + "${SRCROOT}/../../../tests/ios/Pods/Headers/Public/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native-firebase/ios/**", + "$(SRCROOT)/../../../packages/app/ios/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2744B98821F45429004F8E3F /* Build configuration list for PBXNativeTarget "RNFBAppDistribution" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2744B98921F45429004F8E3F /* Debug */, + 2744B98A21F45429004F8E3F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3323F1C5716BA966BBBB95A4 /* Build configuration list for PBXProject "RNFBAppDistribution" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3323F7E33E1559A2B9826720 /* Debug */, + 3323F77D701E1896E6D239CF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3323F95273A95DB34F55C6D7 /* Project object */; +} diff --git a/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/xcshareddata/IDETemplateMacros.plist b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000000..63f0a6e5dd --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,24 @@ + + + + + FILEHEADER + +/** + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + diff --git a/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.h b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.h new file mode 100644 index 0000000000..b21dbecb14 --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#import + +#import + +@interface RNFBAppDistributionModule : NSObject + +@end diff --git a/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m new file mode 100644 index 0000000000..904a626c72 --- /dev/null +++ b/packages/app-distribution/ios/RNFBAppDistribution/RNFBAppDistributionModule.m @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#import +#import + +#import "RNFBApp/RNFBSharedUtils.h" +#import "RNFBAppDistributionModule.h" + +@implementation RNFBAppDistributionModule +#pragma mark - +#pragma mark Module Setup + +RCT_EXPORT_MODULE(); + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + +#pragma mark - +#pragma mark Firebase AppDistribution Methods + +RCT_EXPORT_METHOD(isTesterSignedIn + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + FIRAppDistribution *appDistribution = [FIRAppDistribution appDistribution]; + BOOL isTesterSignedIn = appDistribution.isTesterSignedIn; + resolve([NSNumber numberWithBool:isTesterSignedIn]); +} + +RCT_EXPORT_METHOD(signInTester : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { + FIRAppDistribution *appDistribution = [FIRAppDistribution appDistribution]; + [appDistribution signInTesterWithCompletion:^(NSError *_Nullable error) { + if (error != nil) { + // Handle any errors if the signIn failed + DLog(@"Unable to signInTester: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"tester-sign-in-error", + @"message" : [error localizedDescription], + }]; + return; + } + + resolve([NSNull null]); + }]; +} + +RCT_EXPORT_METHOD(signOutTester + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + [[FIRAppDistribution appDistribution] signOutTester]; + resolve([NSNull null]); +} + +RCT_EXPORT_METHOD(checkForUpdate + : (RCTPromiseResolveBlock)resolve + : (RCTPromiseRejectBlock)reject) { + FIRAppDistribution *appDistribution = [FIRAppDistribution appDistribution]; + [appDistribution checkForUpdateWithCompletion:^(FIRAppDistributionRelease *_Nullable release, + NSError *_Nullable error) { + if (error != nil) { + // Handle any errors if the release was not retrieved. + DLog(@"Unable to check App Distribution release: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"check-update-error", + @"message" : [error localizedDescription], + }]; + return; + } + if (release == nil) { + DLog(@"Unable to check App Distribution release: %@", error); + [RNFBSharedUtils rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"checkupdate-null", + @"message" : @"no update checked", + }]; + return; + } + + resolve(@{ + @"displayVersion" : release.displayVersion, + @"buildVersion" : release.buildVersion, + @"releaseNotes" : release.releaseNotes, + @"isExpired" : [NSNumber numberWithBool:release.isExpired], + @"downloadURL" : release.downloadURL + }); + + resolve([NSNull null]); // FIXME read the result into a map and return it! + }]; +} + +@end diff --git a/packages/app-distribution/lib/index.d.ts b/packages/app-distribution/lib/index.d.ts new file mode 100644 index 0000000000..3ee54e2165 --- /dev/null +++ b/packages/app-distribution/lib/index.d.ts @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ReactNativeFirebase } from '@react-native-firebase/app'; + +/** + * Firebase AppDistribution package for React Native. + * + * #### Example 1 + * + * Access the firebase export from the `app-distribution` package: + * + * ```js + * import { firebase } from '@react-native-firebase/app-distribution'; + * + * // firebase.appDistribution().X + * ``` + * + * #### Example 2 + * + * Using the default export from the `app-distribution` package: + * + * ```js + * import appDistribution from '@react-native-firebase/app-distribution'; + * + * // appDistribution().X + * ``` + * + * #### Example 3 + * + * Using the default export from the `app` package: + * + * ```js + * import firebase from '@react-native-firebase/app'; + * import '@react-native-firebase/app-distribution'; + * + * // firebase.appDistribution().X + * ``` + * + * @firebase app-distribution + */ +export namespace FirebaseAppDistributionTypes { + import FirebaseModule = ReactNativeFirebase.FirebaseModule; + + /** + * The release information returned by the update check when a new version is available. + */ + export interface AppDistributionRelease { + /** + * The short bundle version of this build (example 1.0.0). + */ + displayVersion: string; + + /** + * The build number of this build (example: 123). + */ + buildVersion: string; + + /** + * The release notes for this build. + */ + releaseNotes: string; + + /** + * The URL for the build. + */ + downloadURL: string; + + /** + * Whether the download URL for this release is expired. + */ + isExpired: boolean; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Statics { + // firebase.appDistribution.* static props go here + } + + /** + * The Firebase AppDistribution service interface. + * + * > This module is available for the default app only. + * + * #### Example + * + * Get the AppDistribution service for the default app: + * + * ```js + * const defaultAppAppDistribution = firebase.appDistribution(); + * ``` + */ + export class Module extends FirebaseModule { + /** + * Returns true if the App Distribution tester is signed in. + * If not an iOS device, it always rejects, as neither false nor true seem like a sensible default. + */ + isTesterSignedIn(): Promise; + + /** + * Sign-in the App Distribution tester + * If not an iOS device, it always rejects, as no defaults seem sensible. + */ + signInTester(): Promise; + + /** + * Check to see whether a new distribution is available + * If not an iOS device, it always rejects, as no default response seems sensible. + */ + checkForUpdate(): Promise; + + /** + * Sign out App Distribution tester + * If not an iOS device, it always rejects, as no default response seems sensible. + */ + signOutTester(): Promise; + } +} + +declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< + FirebaseAppDistributionTypes.Module, + FirebaseAppDistributionTypes.Statics +>; + +export const firebase: ReactNativeFirebase.Module & { + appDistribution: typeof defaultExport; + app( + name?: string, + ): ReactNativeFirebase.FirebaseApp & { appDistribution(): FirebaseAppDistributionTypes.Module }; +}; + +export default defaultExport; + +/** + * Attach namespace to `firebase.` and `FirebaseApp.`. + */ +declare module '@react-native-firebase/app' { + namespace ReactNativeFirebase { + import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; + interface Module { + appDistribution: FirebaseModuleWithStaticsAndApp< + FirebaseAppDistributionTypes.Module, + FirebaseAppDistributionTypes.Statics + >; + } + interface FirebaseApp { + appDistribution(): FirebaseAppDistributionTypes.Module; + } + } +} diff --git a/packages/app-distribution/lib/index.js b/packages/app-distribution/lib/index.js new file mode 100644 index 0000000000..cb667341db --- /dev/null +++ b/packages/app-distribution/lib/index.js @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { isIOS } from '@react-native-firebase/app/lib/common'; +import { + createModuleNamespace, + FirebaseModule, + getFirebaseRoot, +} from '@react-native-firebase/app/lib/internal'; + +import version from './version'; + +const statics = {}; + +const namespace = 'appDistribution'; + +const nativeModuleName = 'RNFBAppDistributionModule'; + +class FirebaseAppDistributionModule extends FirebaseModule { + isTesterSignedIn() { + if (isIOS) { + return this.native.isTesterSignedIn(); + } + + Promise.reject(new Error('App Distribution is not supported on this platform.')); + } + + signInTester() { + if (isIOS) { + return this.native.signInTester(); + } + + Promise.reject(new Error('App Distribution is not supported on this platform.')); + } + + checkForUpdate() { + if (isIOS) { + return this.native.checkForUpdate(); + } + + Promise.reject(new Error('App Distribution is not supported on this platform.')); + } + + signOutTester() { + if (isIOS) { + return this.native.signOutTester(); + } + + Promise.reject(new Error('App Distribution is not supported on this platform.')); + } +} + +// import { SDK_VERSION } from '@react-native-firebase/app-distribution'; +export const SDK_VERSION = version; + +// import appDistribution from '@react-native-firebase/app-distribution'; +// appDistribution().X(...); +export default createModuleNamespace({ + statics, + version, + namespace, + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, + ModuleClass: FirebaseAppDistributionModule, +}); + +// import appDistribution, { firebase } from '@react-native-firebase/app-distribution'; +// appDistribution().X(...); +// firebase.appDistribution().X(...); +export const firebase = getFirebaseRoot(); diff --git a/packages/app-distribution/package.json b/packages/app-distribution/package.json new file mode 100644 index 0000000000..acfcf652ed --- /dev/null +++ b/packages/app-distribution/package.json @@ -0,0 +1,30 @@ +{ + "name": "@react-native-firebase/app-distribution", + "version": "12.6.1", + "author": "Invertase (http://invertase.io)", + "description": "React Native Firebase - App Distribution", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "genversion --semi lib/version.js", + "build:clean": "rimraf android/build && rimraf ios/build", + "prepare": "yarn run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/invertase/react-native-firebase/tree/master/packages/app-distribution" + }, + "license": "Apache-2.0", + "keywords": [ + "react", + "react-native", + "firebase", + "app-distribution" + ], + "peerDependencies": { + "@react-native-firebase/app": "12.6.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/app-distribution/type-test.ts b/packages/app-distribution/type-test.ts new file mode 100644 index 0000000000..8259ba2f18 --- /dev/null +++ b/packages/app-distribution/type-test.ts @@ -0,0 +1,26 @@ +import firebase from '@react-native-firebase/app'; +import defaultExport, { firebase as firebaseFromModule } from '@react-native-firebase/app-distribution'; + +// checks module exists at root +console.log(firebase.appDistribution().app.name); + +// checks module exists at app level +console.log(firebase.app().appDistribution().app.name); + +// checks statics exist +console.log(firebase.appDistribution.SDK_VERSION); + +// checks statics exist on defaultExport +console.log(defaultExport.SDK_VERSION); + +// checks root exists +console.log(firebase.SDK_VERSION); + +// checks firebase named export exists on module +console.log(firebaseFromModule.SDK_VERSION); + +// checks multi-app support exists +console.log(firebase.appDistribution(firebase.app()).app.name); + +// checks default export supports app arg +console.log(defaultExport(firebase.app()).app.name); diff --git a/packages/app/lib/internal/constants.js b/packages/app/lib/internal/constants.js index aae6cb3d62..b272015e5d 100644 --- a/packages/app/lib/internal/constants.js +++ b/packages/app/lib/internal/constants.js @@ -21,6 +21,7 @@ export const DEFAULT_APP_NAME = '[DEFAULT]'; export const KNOWN_NAMESPACES = [ 'appCheck', + 'appDistribution', 'auth', 'analytics', 'remoteConfig', diff --git a/tests/app.js b/tests/app.js index 03ff6abb04..c66baf74db 100644 --- a/tests/app.js +++ b/tests/app.js @@ -20,6 +20,7 @@ import firebase from '@react-native-firebase/app'; import NativeEventEmitter from '@react-native-firebase/app/lib/internal/RNFBNativeEventEmitter'; import '@react-native-firebase/app/lib/utils'; import '@react-native-firebase/app-check'; +import '@react-native-firebase/app-distribution'; import '@react-native-firebase/auth'; import '@react-native-firebase/crashlytics'; import '@react-native-firebase/database'; diff --git a/tests/e2e/.mocharc.js b/tests/e2e/.mocharc.js index 89631fb6ac..8ef68ce752 100644 --- a/tests/e2e/.mocharc.js +++ b/tests/e2e/.mocharc.js @@ -13,6 +13,7 @@ module.exports = { spec: [ '../packages/app/e2e/**/*.e2e.js', '../packages/app-check/e2e/**/*.e2e.js', + '../packages/app-distribution/e2e/**/*.e2e.js', '../packages/analytics/e2e/**/*.e2e.js', '../packages/auth/e2e/**/*.e2e.js', '../packages/crashlytics/e2e/**/*.e2e.js', diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index d6e1c07b3b..b7c4978106 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -359,6 +359,9 @@ PODS: - Firebase/AppCheck (8.6.0): - Firebase/CoreOnly - FirebaseAppCheck (~> 8.6.0-beta) + - Firebase/AppDistribution (8.6.0): + - Firebase/CoreOnly + - FirebaseAppDistribution (~> 8.6.0-beta) - Firebase/Auth (8.6.0): - Firebase/CoreOnly - FirebaseAuth (~> 8.6.0) @@ -424,6 +427,12 @@ PODS: - FirebaseCore (~> 8.0) - GoogleUtilities/Environment (~> 7.4) - PromisesObjC (< 3.0, >= 1.2) + - FirebaseAppDistribution (8.6.0-beta): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.4) + - GoogleUtilities/UserDefaults (~> 7.4) - FirebaseAuth (8.6.0): - FirebaseCore (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.4) @@ -844,6 +853,10 @@ PODS: - Firebase/AppCheck (= 8.6.0) - React-Core - RNFBApp + - RNFBAppDistribution (12.6.1): + - Firebase/AppDistribution (= 8.6.0) + - React-Core + - RNFBApp - RNFBAuth (12.6.1): - Firebase/Auth (= 8.6.0) - React-Core @@ -932,6 +945,7 @@ DEPENDENCIES: - RNFBAnalytics (from `../../packages/analytics`) - RNFBApp (from `../../packages/app`) - RNFBAppCheck (from `../../packages/app-check`) + - RNFBAppDistribution (from `../../packages/app-distribution`) - RNFBAuth (from `../../packages/auth`) - RNFBCrashlytics (from `../../packages/crashlytics`) - RNFBDatabase (from `../../packages/database`) @@ -956,6 +970,7 @@ SPEC REPOS: - FirebaseABTesting - FirebaseAnalytics - FirebaseAppCheck + - FirebaseAppDistribution - FirebaseAuth - FirebaseCore - FirebaseCoreDiagnostics @@ -1043,6 +1058,8 @@ EXTERNAL SOURCES: :path: "../../packages/app" RNFBAppCheck: :path: "../../packages/app-check" + RNFBAppDistribution: + :path: "../../packages/app-distribution" RNFBAuth: :path: "../../packages/auth" RNFBCrashlytics: @@ -1083,6 +1100,7 @@ SPEC CHECKSUMS: FirebaseABTesting: c3e48ebf5e7e5c674c5a131c68e941d7921d83dc FirebaseAnalytics: 8f32ae54ad42754f503354782575c4ddfc1425c3 FirebaseAppCheck: 24bd8311c6dbfd1b03b1c7a3cb7d5a64dbc0cd4f + FirebaseAppDistribution: 91f5b721d665aa88dc8d98410eba1ee22b1691a9 FirebaseAuth: 223adeeb2262b417532e89bf06a960e3a0a1e9e4 FirebaseCore: 620b677f70f5470a8e59cb77f3ddc666f6f09785 FirebaseCoreDiagnostics: 3721920bde3a9a6d5aa093c1d25e9d3e47f694af @@ -1134,6 +1152,7 @@ SPEC CHECKSUMS: RNFBAnalytics: d52a31e8683fd1b6c5667580481676a751d52484 RNFBApp: 884c958fe3256d2cb99cc92f54f00a319cc0aed6 RNFBAppCheck: f315f2d159cc72657d0742d196c48ca490e4da17 + RNFBAppDistribution: e2b4697101f15b28dfcec36c6d89c41fe4ce39f4 RNFBAuth: b2ce9c35a5d86149e5aa77ef78392a4100fc676a RNFBCrashlytics: 149a89db16522c94d62b4ff3a065e040d3f58f0a RNFBDatabase: a172802ce1aba9219d1881126ef10687b1ed6c82 diff --git a/tests/package.json b/tests/package.json index a5092ad82a..ca7c1edd6c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -10,6 +10,7 @@ "@react-native-firebase/analytics": "12.6.1", "@react-native-firebase/app": "12.6.1", "@react-native-firebase/app-check": "12.6.1", + "@react-native-firebase/app-distribution": "12.6.1", "@react-native-firebase/app-types": "6.7.2", "@react-native-firebase/auth": "12.6.1", "@react-native-firebase/crashlytics": "12.6.1", diff --git a/website/scripts/source-reference.js b/website/scripts/source-reference.js index 82df859b22..e7c6a7317e 100644 --- a/website/scripts/source-reference.js +++ b/website/scripts/source-reference.js @@ -147,6 +147,8 @@ function moduleNameToFullName(name) { return 'AdMob'; case 'app-check': return 'App Check'; + case 'app-distribution': + return 'App Distribution'; case 'analytics': return 'Analytics'; case 'auth': diff --git a/website/src/templates/utils.ts b/website/src/templates/utils.ts index 91c7772052..adf596ca96 100644 --- a/website/src/templates/utils.ts +++ b/website/src/templates/utils.ts @@ -29,6 +29,8 @@ function iconForModule(module: string): string { return '//static.invertase.io/assets/firebase/analytics.svg'; case 'app-check': return '//static.invertase.io/assets/social/firebase-logo.png'; + case 'app-distribution': + return '/assets/docs/app-distribution/app-distribution.png'; case 'auth': return '//static.invertase.io/assets/firebase/authentication.svg'; case 'firestore': diff --git a/website/static/assets/docs/app-distribution/app-distribution.png b/website/static/assets/docs/app-distribution/app-distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..dbc932ea7d174e1a045ca9fa01e37e67f54e0524 GIT binary patch literal 11644 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 G#Qy