diff --git a/.changeset/appauth-android-request-lifecycle.md b/.changeset/appauth-android-request-lifecycle.md new file mode 100644 index 000000000..a6690a1ed --- /dev/null +++ b/.changeset/appauth-android-request-lifecycle.md @@ -0,0 +1,5 @@ +--- +"react-native-app-auth": patch +--- + +Replace Android's blocking, global prefetch latch with per-issuer asynchronous completion and expose native prefetch completion and errors through the JavaScript promise. diff --git a/docs/docs/usage/prefetch.md b/docs/docs/usage/prefetch.md index e16b8c4f5..b27ad1ed1 100644 --- a/docs/docs/usage/prefetch.md +++ b/docs/docs/usage/prefetch.md @@ -17,5 +17,13 @@ const config = { scopes: [''], }; -prefetchConfiguration(config); +try { + await prefetchConfiguration(config); +} catch (error) { + // Prefetch is optional. authorize() can retry discovery when needed. +} ``` + +The promise resolves only after configuration is available and rejects when discovery fails. +Cached issuers resolve immediately; prefetching a different issuer fetches its own configuration. +Calls on iOS remain a no-op. Handle rejection if you previously called this method without awaiting it. diff --git a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java index f1e955879..51ef3bfc0 100644 --- a/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java +++ b/packages/react-native-app-auth/android/src/main/java/com/rnappauth/RNAppAuthModule.java @@ -64,7 +64,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; public class RNAppAuthModule extends ReactContextBaseJavaModule implements ActivityEventListener { @@ -84,7 +83,6 @@ public class RNAppAuthModule extends ReactContextBaseJavaModule implements Activ private Map additionalParametersMap; private String clientSecret; private final ConcurrentHashMap mServiceConfigurations = new ConcurrentHashMap<>(); - private boolean isPrefetched = false; public RNAppAuthModule(ReactApplicationContext reactContext) { super(reactContext); @@ -111,47 +109,36 @@ public void prefetchConfiguration( this.parseHeaderMap(customHeaders); final ConnectionBuilder builder = createConnectionBuilder(dangerouslyAllowInsecureHttpRequests, this.authorizationRequestHeaders, connectionTimeoutMillis); - final CountDownLatch fetchConfigurationLatch = new CountDownLatch(1); - - if (!isPrefetched) { - if (serviceConfiguration != null && !this.hasServiceConfiguration(issuer)) { - try { - setServiceConfiguration(issuer, createAuthorizationServiceConfiguration(serviceConfiguration)); - isPrefetched = true; - fetchConfigurationLatch.countDown(); - } catch (Exception e) { - promise.reject("configuration_error", "Failed to convert serviceConfiguration", e); - } - } else if (!hasServiceConfiguration(issuer)) { - final Uri issuerUri = Uri.parse(issuer); - AuthorizationServiceConfiguration.fetchFromUrl( - buildConfigurationUriFromIssuer(issuerUri), - new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { - public void onFetchConfigurationCompleted( - @Nullable AuthorizationServiceConfiguration fetchedConfiguration, - @Nullable AuthorizationException ex) { - if (ex != null) { - promise.reject("service_configuration_fetch_error", "Failed to fetch configuration", - ex); - return; - } - setServiceConfiguration(issuer, fetchedConfiguration); - isPrefetched = true; - fetchConfigurationLatch.countDown(); - } - }, - builder); - } - } else { - fetchConfigurationLatch.countDown(); + if (hasServiceConfiguration(issuer)) { + promise.resolve(true); + return; } - try { - fetchConfigurationLatch.await(); - promise.resolve(isPrefetched); - } catch (Exception e) { - promise.reject("service_configuration_fetch_error", "Failed to await fetch configuration", e); + if (serviceConfiguration != null) { + try { + setServiceConfiguration(issuer, createAuthorizationServiceConfiguration(serviceConfiguration)); + promise.resolve(true); + } catch (Exception e) { + promise.reject("configuration_error", "Failed to convert serviceConfiguration", e); + } + return; } + + AuthorizationServiceConfiguration.fetchFromUrl( + buildConfigurationUriFromIssuer(Uri.parse(issuer)), + new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { + public void onFetchConfigurationCompleted( + @Nullable AuthorizationServiceConfiguration fetchedConfiguration, + @Nullable AuthorizationException ex) { + if (ex != null) { + promise.reject("service_configuration_fetch_error", "Failed to fetch configuration", ex); + return; + } + setServiceConfiguration(issuer, fetchedConfiguration); + promise.resolve(true); + } + }, + builder); } @ReactMethod diff --git a/packages/react-native-app-auth/index.d.ts b/packages/react-native-app-auth/index.d.ts index 7ff7ec62b..4260b9f26 100644 --- a/packages/react-native-app-auth/index.d.ts +++ b/packages/react-native-app-auth/index.d.ts @@ -182,6 +182,7 @@ type OAuthTokenErrorCode = // https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError type OICRegistrationErrorCode = 'invalid_redirect_uri' | 'invalid_client_metadata'; type AppAuthErrorCode = + | 'configuration_error' | 'service_configuration_fetch_error' | 'authentication_failed' | 'token_refresh_failed' diff --git a/packages/react-native-app-auth/index.js b/packages/react-native-app-auth/index.js index a49c3c58c..9c28c070f 100644 --- a/packages/react-native-app-auth/index.js +++ b/packages/react-native-app-auth/index.js @@ -140,7 +140,7 @@ export const prefetchConfiguration = async ({ convertTimeoutForPlatform(Platform.OS, connectionTimeoutSeconds), ]; - RNAppAuth.prefetchConfiguration(...nativeMethodArguments); + await wrapNativeAuthPromise(RNAppAuth.prefetchConfiguration(...nativeMethodArguments)); } };