From adebe40883adc539bf84134d407be677084a5b47 Mon Sep 17 00:00:00 2001 From: "Ibrahim H. Sluma" Date: Mon, 4 Sep 2023 23:38:28 +0200 Subject: [PATCH] feat(app-check, android): Implement app check token change listener (#7309) * feat(app-check, android) Add onTokenChanged listener * style(lint): `yarn lint:js --fix` --------- Co-authored-by: Mike Hardy --- .../ReactNativeFirebaseAppCheckModule.java | 67 +++++++++++++++++++ packages/app-check/lib/index.d.ts | 37 +++++++--- packages/app-check/lib/index.js | 45 +++++++++++-- 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java b/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java index 5f99bb236a..5ad88729ec 100644 --- a/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java +++ b/packages/app-check/android/src/main/java/io/invertase/firebase/appcheck/ReactNativeFirebaseAppCheckModule.java @@ -28,11 +28,19 @@ import io.invertase.firebase.common.ReactNativeFirebaseMeta; import io.invertase.firebase.common.ReactNativeFirebaseModule; import io.invertase.firebase.common.ReactNativeFirebasePreferences; +import io.invertase.firebase.common.ReactNativeFirebaseEvent; +import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; public class ReactNativeFirebaseAppCheckModule extends ReactNativeFirebaseModule { private static final String TAG = "AppCheck"; private static final String LOGTAG = "RNFBAppCheck"; private static final String KEY_APPCHECK_TOKEN_REFRESH_ENABLED = "app_check_token_auto_refresh"; + + private static HashMap mAppCheckListeners = new HashMap<>(); + ReactNativeFirebaseAppCheckProviderFactory providerFactory = new ReactNativeFirebaseAppCheckProviderFactory(); @@ -90,6 +98,24 @@ private boolean isAppDebuggable() throws Exception { firebaseAppCheck.setTokenAutoRefreshEnabled(isAppCheckTokenRefreshEnabled()); } + @Override + public void onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy(); + Log.d(TAG, "instance-destroyed"); + + Iterator appCheckListenerIterator = mAppCheckListeners.entrySet().iterator(); + + while (appCheckListenerIterator.hasNext()) { + Map.Entry pair = (Map.Entry) appCheckListenerIterator.next(); + String appName = (String) pair.getKey(); + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance(firebaseApp); + FirebaseAppCheck.AppCheckListener mAppCheckListener = (FirebaseAppCheck.AppCheckListener) pair.getValue(); + firebaseAppCheck.removeAppCheckListener(mAppCheckListener); + appCheckListenerIterator.remove(); + } + } + @ReactMethod public void configureProvider( String appName, String providerName, String debugToken, Promise promise) { @@ -177,4 +203,45 @@ public void getToken(String appName, boolean forceRefresh, Promise promise) { } }); } + + /** Add a new token change listener - if one doesn't exist already */ + @ReactMethod + public void addAppCheckListener(final String appName) { + Log.d(TAG, "addAppCheckListener " + appName); + + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance(firebaseApp); + + if (mAppCheckListeners.get(appName) == null) { + FirebaseAppCheck.AppCheckListener newAppCheckListener = appCheckToken -> { + WritableMap eventBody = Arguments.createMap(); + eventBody.putString("appName", appName); // for js side distribution + eventBody.putString("token", appCheckToken.getToken()); + eventBody.putDouble("expireTimeMillis", appCheckToken.getExpireTimeMillis()); + + ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance(); + ReactNativeFirebaseEvent event = new ReactNativeFirebaseEvent("appCheck_token_changed", eventBody, appName); + emitter.sendEvent(event); + }; + + firebaseAppCheck.addAppCheckListener(newAppCheckListener); + mAppCheckListeners.put(appName, newAppCheckListener); + } + } + + /** Removes the current token change listener */ + @ReactMethod + public void removeAppCheckListener(String appName) { + Log.d(TAG, "removeAppCheckListener " + appName); + + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance(firebaseApp); + + FirebaseAppCheck.AppCheckListener mAppCheckListener = mAppCheckListeners.get(appName); + + if (mAppCheckListener != null) { + firebaseAppCheck.removeAppCheckListener(mAppCheckListener); + mAppCheckListeners.remove(appName); + } + } } diff --git a/packages/app-check/lib/index.d.ts b/packages/app-check/lib/index.d.ts index 146a66671b..329da83b1a 100644 --- a/packages/app-check/lib/index.d.ts +++ b/packages/app-check/lib/index.d.ts @@ -88,6 +88,18 @@ export namespace FirebaseAppCheckTypes { isTokenAutoRefreshEnabled?: boolean; } + export type NextFn = (value: T) => void; + export type ErrorFn = (error: Error) => void; + export type CompleteFn = () => void; + + export interface Observer { + next: NextFn; + error: ErrorFn; + complete: CompleteFn; + } + + export type PartialObserver = Partial>; + export interface ReactNativeFirebaseAppCheckProviderOptions { /** * debug token to use, if any. Defaults to undefined, pre-configure tokens in firebase web console if needed @@ -165,6 +177,10 @@ export namespace FirebaseAppCheckTypes { */ readonly expireTimeMillis: number; } + /** + * The result return from `onTokenChanged` + */ + export type AppCheckListenerResult = AppCheckToken & { readonly appName: string }; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Statics { @@ -258,12 +274,9 @@ export namespace FirebaseAppCheckTypes { * * @returns A function that unsubscribes this listener. */ - // TODO there is a great deal of Observer / PartialObserver typing to carry-in - // onTokenChanged(observer: PartialObserver): () => void; + onTokenChanged(observer: PartialObserver): () => void; /** - * TODO implement token listener for android. - * * Registers a listener to changes in the token state. There can be more * than one listener registered at the same time for one or more * App Check instances. The listeners call back on the UI thread whenever @@ -272,13 +285,19 @@ export namespace FirebaseAppCheckTypes { * Token listeners do not exist in the native SDK for iOS, no token change events will be emitted on that platform. * This is not yet implemented on Android, no token change events will be emitted until implemented. * + * NOTE: Although an `onError` callback can be provided, it will + * never be called, Android sdk code doesn't provide handling for onError function + * + * NOTE: Although an `onCompletion` callback can be provided, it will + * never be called because the token stream is never-ending. + * * @returns A function that unsubscribes this listener. */ - // onTokenChanged( - // onNext: (tokenResult: AppCheckTokenResult) => void, - // onError?: (error: Error) => void, - // onCompletion?: () => void, - // ): () => void; + onTokenChanged( + onNext: (tokenResult: AppCheckListenerResult) => void, + onError?: (error: Error) => void, + onCompletion?: () => void, + ): () => void; } } diff --git a/packages/app-check/lib/index.js b/packages/app-check/lib/index.js index d1fe7fc9ec..b68734ad6a 100644 --- a/packages/app-check/lib/index.js +++ b/packages/app-check/lib/index.js @@ -40,6 +40,16 @@ const namespace = 'appCheck'; const nativeModuleName = 'RNFBAppCheckModule'; class FirebaseAppCheckModule extends FirebaseModule { + constructor(...args) { + super(...args); + + this.emitter.addListener(this.eventNameForApp('appCheck_token_changed'), event => { + this.emitter.emit(this.eventNameForApp('onAppCheckTokenChanged'), event); + }); + + this._listenerCount = 0; + } + getIsTokenRefreshEnabledDefault() { // no default to start isTokenAutoRefreshEnabled = undefined; @@ -131,13 +141,40 @@ class FirebaseAppCheckModule extends FirebaseModule { } } - onTokenChanged() { + _parseListener(listenerOrObserver) { + return typeof listenerOrObserver === 'object' + ? listenerOrObserver.next.bind(listenerOrObserver) + : listenerOrObserver; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onTokenChanged(onNextOrObserver, onError, onCompletion) { // 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 () => {}; } - // TODO unimplemented on Android - return () => {}; + const nextFn = this._parseListener(onNextOrObserver); + // 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(); + }; } } @@ -151,7 +188,7 @@ export default createModuleNamespace({ version, namespace, nativeModuleName, - nativeEvents: false, // TODO implement ['appcheck-token-changed'], + nativeEvents: ['appCheck_token_changed'], hasMultiAppSupport: true, hasCustomUrlOrRegionSupport: false, ModuleClass: FirebaseAppCheckModule,