Skip to content

Commit

Permalink
feat(app-check, android): Implement app check token change listener (#…
Browse files Browse the repository at this point in the history
…7309)

* feat(app-check, android) Add onTokenChanged listener

* style(lint): `yarn lint:js --fix`

---------

Co-authored-by: Mike Hardy <[email protected]>
  • Loading branch information
MrLibya and mikehardy authored Sep 4, 2023
1 parent b6f64bf commit adebe40
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, FirebaseAppCheck.AppCheckListener> mAppCheckListeners = new HashMap<>();

ReactNativeFirebaseAppCheckProviderFactory providerFactory =
new ReactNativeFirebaseAppCheckProviderFactory();

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
}
37 changes: 28 additions & 9 deletions packages/app-check/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ export namespace FirebaseAppCheckTypes {
isTokenAutoRefreshEnabled?: boolean;
}

export type NextFn<T> = (value: T) => void;
export type ErrorFn = (error: Error) => void;
export type CompleteFn = () => void;

export interface Observer<T> {
next: NextFn<T>;
error: ErrorFn;
complete: CompleteFn;
}

export type PartialObserver<T> = Partial<Observer<T>>;

export interface ReactNativeFirebaseAppCheckProviderOptions {
/**
* debug token to use, if any. Defaults to undefined, pre-configure tokens in firebase web console if needed
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<AppCheckTokenResult>): () => void;
onTokenChanged(observer: PartialObserver<AppCheckListenerResult>): () => 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
Expand All @@ -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;
}
}

Expand Down
45 changes: 41 additions & 4 deletions packages/app-check/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
};
}
}

Expand All @@ -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,
Expand Down

1 comment on commit adebe40

@vercel
Copy link

@vercel vercel bot commented on adebe40 Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.