/* * 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 { isBoolean, isIOS, isString, isObject, isUndefined, isOther, parseListenerOrObserver, } from '@react-native-firebase/app/dist/module/common'; import { createModuleNamespace, FirebaseModule, getFirebaseRoot, } from '@react-native-firebase/app/dist/module/internal'; import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; import { Platform } from 'react-native'; import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; import fallBackModule from './web/RNFBAppCheckModule'; import { version } from './version'; import type { AppCheckOptions, AppCheckProvider, AppCheckTokenResult, PartialObserver, } from './types/appcheck'; import type { ProviderWithOptions } from './types/internal'; import type { FirebaseAppCheckTypes } from './types/namespaced'; import type { ReactNativeFirebase } from '@react-native-firebase/app'; import { CustomProvider, ReactNativeFirebaseAppCheckProvider } from './providers'; const namespace = 'appCheck'; const nativeModuleName = 'RNFBAppCheckModule'; const statics = { CustomProvider, }; /** * Type guard to check if a provider has providerOptions. * This provides proper type narrowing for providers that support platform-specific configuration. */ function hasProviderOptions( provider: AppCheckOptions['provider'], ): provider is ProviderWithOptions { return ( provider !== undefined && 'providerOptions' in provider && provider.providerOptions !== undefined ); } class FirebaseAppCheckModule extends FirebaseModule { _listenerCount: number; constructor( app: ReactNativeFirebase.FirebaseAppBase, config: ModuleConfig, customUrlOrRegion?: string | null, ) { super(app, config, customUrlOrRegion); this.emitter.addListener(this.eventNameForApp('appCheck_token_changed'), event => { this.emitter.emit(this.eventNameForApp('onAppCheckTokenChanged'), event); }); this._listenerCount = 0; } getIsTokenRefreshEnabledDefault(): boolean | undefined { // no default to start let isTokenAutoRefreshEnabled: boolean | undefined = undefined; return isTokenAutoRefreshEnabled; } newReactNativeFirebaseAppCheckProvider(): ReactNativeFirebaseAppCheckProvider { return new ReactNativeFirebaseAppCheckProvider(); } initializeAppCheck(options: AppCheckOptions): Promise { if (isOther) { if (!isObject(options)) { throw new Error('Invalid configuration: no options defined.'); } if (isUndefined(options.provider)) { throw new Error('Invalid configuration: no provider defined.'); } return this.native.initializeAppCheck(options); } // determine token refresh setting, if not specified if (!isBoolean(options.isTokenAutoRefreshEnabled)) { const tokenRefresh = this.firebaseJson.app_check_token_auto_refresh; if (isBoolean(tokenRefresh)) { options.isTokenAutoRefreshEnabled = tokenRefresh; } } // If that was not defined, attempt to use app-wide data collection setting per docs: if (!isBoolean(options.isTokenAutoRefreshEnabled)) { const dataCollection = this.firebaseJson.app_data_collection_default_enabled; if (isBoolean(dataCollection)) { options.isTokenAutoRefreshEnabled = dataCollection; } } // If that also was not defined, the default is documented as true. if (!isBoolean(options.isTokenAutoRefreshEnabled)) { options.isTokenAutoRefreshEnabled = true; } this.native.setTokenAutoRefreshEnabled(options.isTokenAutoRefreshEnabled); if (!hasProviderOptions(options.provider)) { throw new Error('Invalid configuration: no provider or no provider options defined.'); } const provider = options.provider; if (Platform.OS === 'android') { if (!isString(provider.providerOptions?.android?.provider)) { throw new Error( 'Invalid configuration: no android provider configured while on android platform.', ); } return this.native.configureProvider( provider.providerOptions.android.provider, provider.providerOptions.android.debugToken, ); } if (Platform.OS === 'ios' || Platform.OS === 'macos') { if (!isString(provider.providerOptions?.apple?.provider)) { throw new Error( 'Invalid configuration: no apple provider configured while on apple platform.', ); } return this.native.configureProvider( provider.providerOptions.apple.provider, provider.providerOptions.apple.debugToken, ); } throw new Error('Unsupported platform: ' + Platform.OS); } activate( siteKeyOrProvider: string | AppCheckProvider, isTokenAutoRefreshEnabled?: boolean, ): Promise { if (isOther) { throw new Error('firebase.appCheck().activate(*) is not supported on other platforms'); } if (!isString(siteKeyOrProvider)) { throw new Error('siteKeyOrProvider must be a string value to match firebase-js-sdk API'); } // We wrap our new flexible interface, with compatible defaults const rnfbProvider = new ReactNativeFirebaseAppCheckProvider(); rnfbProvider.configure({ android: { provider: 'playIntegrity', }, apple: { provider: 'deviceCheck', }, web: { provider: 'reCaptchaV3', siteKey: 'none', }, }); return this.initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled }); } // TODO this is an async call setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void { this.native.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled); } getToken(forceRefresh?: boolean): Promise { if (!forceRefresh) { return this.native.getToken(false); } else { return this.native.getToken(true); } } getLimitedUseToken(): Promise { return this.native.getLimitedUseToken(); } onTokenChanged( onNextOrObserver: | PartialObserver | ((tokenResult: FirebaseAppCheckTypes.AppCheckListenerResult) => void), _onError?: (error: Error) => void, _onCompletion?: () => void, ): () => void { // iOS does not provide any native listening feature if (isIOS) { // eslint-disable-next-line no-console console.warn('onTokenChanged is not implemented on IOS, only for Android'); return () => {}; } const nextFn = parseListenerOrObserver( onNextOrObserver as | ((value: FirebaseAppCheckTypes.AppCheckListenerResult) => void) | { next: (value: FirebaseAppCheckTypes.AppCheckListenerResult) => void }, ); // let errorFn = function () { }; // if (onNextOrObserver.error != null) { // errorFn = onNextOrObserver.error.bind(onNextOrObserver); // } // else if (onError) { // errorFn = onError; // } const subscription = this.emitter.addListener( this.eventNameForApp('onAppCheckTokenChanged'), nextFn, ); if (this._listenerCount === 0) this.native.addAppCheckListener(); this._listenerCount++; return () => { subscription.remove(); this._listenerCount--; if (this._listenerCount === 0) this.native.removeAppCheckListener(); }; } } export const SDK_VERSION = version; const appCheckNamespace = createModuleNamespace({ statics, version, namespace, nativeModuleName, nativeEvents: ['appCheck_token_changed'], hasMultiAppSupport: true, hasCustomUrlOrRegionSupport: false, ModuleClass: FirebaseAppCheckModule, }); type AppCheckNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< FirebaseAppCheckTypes.Module, FirebaseAppCheckTypes.Statics > & { appCheck: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< FirebaseAppCheckTypes.Module, FirebaseAppCheckTypes.Statics >; firebase: ReactNativeFirebase.Module; app(name?: string): ReactNativeFirebase.FirebaseApp; }; // import appCheck from '@react-native-firebase/app-check'; // appCheck().X(...); export default appCheckNamespace as unknown as AppCheckNamespace; // import appCheck, { firebase } from '@react-native-firebase/app-check'; // appCheck().X(...); // firebase.appCheck().X(...); export const firebase = getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< 'appCheck', FirebaseAppCheckTypes.Module, FirebaseAppCheckTypes.Statics, false >; // Register the interop module for non-native platforms. setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record);