Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/appauth-android-request-lifecycle.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion docs/docs/usage/prefetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,13 @@ const config = {
scopes: ['<YOUR_SCOPES_ARRAY>'],
};

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.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -84,7 +83,6 @@ public class RNAppAuthModule extends ReactContextBaseJavaModule implements Activ
private Map<String, String> additionalParametersMap;
private String clientSecret;
private final ConcurrentHashMap<String, AuthorizationServiceConfiguration> mServiceConfigurations = new ConcurrentHashMap<>();
private boolean isPrefetched = false;

public RNAppAuthModule(ReactApplicationContext reactContext) {
super(reactContext);
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-app-auth/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-app-auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const prefetchConfiguration = async ({
convertTimeoutForPlatform(Platform.OS, connectionTimeoutSeconds),
];

RNAppAuth.prefetchConfiguration(...nativeMethodArguments);
await wrapNativeAuthPromise(RNAppAuth.prefetchConfiguration(...nativeMethodArguments));
}
};

Expand Down