react-native-oauth-essentials 1.0.11 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +384 -362
  2. package/android/src/main/java/com/oauthessentials/CredentialError.kt +4 -1
  3. package/android/src/main/java/com/oauthessentials/CredentialFactory.kt +24 -2
  4. package/android/src/main/java/com/oauthessentials/CustomTabsActivity.kt +14 -1
  5. package/android/src/main/java/com/oauthessentials/OauthEssentialsModule.kt +448 -192
  6. package/app.plugin.js +1 -0
  7. package/ios/AppleIdRetriever.swift +25 -26
  8. package/ios/CredentialError.swift +3 -0
  9. package/ios/CredentialFactory.swift +38 -20
  10. package/ios/CredentialsType.swift +1 -0
  11. package/ios/GoogleIdRetriever.swift +132 -56
  12. package/ios/HybridRetriever.swift +12 -18
  13. package/ios/OauthEssentials.h +0 -3
  14. package/ios/OauthEssentials.mm +45 -2
  15. package/ios/OauthEssentialsModule.swift +266 -57
  16. package/ios/PasskeyRetriever.swift +31 -49
  17. package/ios/PasswordRetriever.swift +10 -30
  18. package/ios/RCTPromiseSettler.swift +125 -0
  19. package/jest/index.js +149 -0
  20. package/lib/module/NativeOauthEssentials.js +10 -1
  21. package/lib/module/NativeOauthEssentials.js.map +1 -1
  22. package/lib/module/errors.js +38 -0
  23. package/lib/module/errors.js.map +1 -0
  24. package/lib/module/index.js +432 -58
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/oauth.js +97 -0
  27. package/lib/module/oauth.js.map +1 -0
  28. package/lib/module/platform.js +108 -0
  29. package/lib/module/platform.js.map +1 -0
  30. package/lib/module/types.js +46 -1
  31. package/lib/module/types.js.map +1 -1
  32. package/lib/typescript/src/NativeOauthEssentials.d.ts +70 -16
  33. package/lib/typescript/src/NativeOauthEssentials.d.ts.map +1 -1
  34. package/lib/typescript/src/errors.d.ts +18 -0
  35. package/lib/typescript/src/errors.d.ts.map +1 -0
  36. package/lib/typescript/src/index.d.ts +235 -40
  37. package/lib/typescript/src/index.d.ts.map +1 -1
  38. package/lib/typescript/src/oauth.d.ts +32 -0
  39. package/lib/typescript/src/oauth.d.ts.map +1 -0
  40. package/lib/typescript/src/platform.d.ts +39 -0
  41. package/lib/typescript/src/platform.d.ts.map +1 -0
  42. package/lib/typescript/src/types.d.ts +190 -3
  43. package/lib/typescript/src/types.d.ts.map +1 -1
  44. package/package.json +8 -4
  45. package/plugin/withOauthEssentials.js +291 -0
  46. package/src/NativeOauthEssentials.ts +91 -17
  47. package/src/errors.ts +49 -0
  48. package/src/index.tsx +587 -80
  49. package/src/oauth.ts +140 -0
  50. package/src/platform.ts +121 -0
  51. package/src/types.ts +200 -2
package/src/oauth.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { createCredentialError } from './errors';
2
+ import { warnOnce } from './platform';
3
+ import { CredentialError } from './types';
4
+
5
+ export type PkcePair = {
6
+ codeVerifier: string;
7
+ codeChallenge: string;
8
+ state: string;
9
+ };
10
+
11
+ export type AuthorizationUrlParams = {
12
+ authorizationEndpoint: string;
13
+ clientId: string;
14
+ redirectUri: string;
15
+ scopes?: string[];
16
+ additionalParameters?: Record<string, string>;
17
+ state: string;
18
+ codeChallenge?: string;
19
+ };
20
+
21
+ function encodeQuery(params: Record<string, string>): string {
22
+ return Object.keys(params)
23
+ .map(
24
+ (key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key]!)}`
25
+ )
26
+ .join('&');
27
+ }
28
+
29
+ /**
30
+ * Decodes a query component, keeping it raw when its percent-encoding is malformed (`50%off`), so
31
+ * a provider bug does not turn into an error without `code`.
32
+ */
33
+ function decodeComponent(value: string): string {
34
+ const spaced = value.replace(/\+/g, ' ');
35
+ try {
36
+ return decodeURIComponent(spaced);
37
+ } catch {
38
+ return spaced;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Set by the library and checked or reused later (state, PKCE, token exchange), so
44
+ * `additionalParameters` cannot override them.
45
+ */
46
+ export const RESERVED_AUTHORIZATION_PARAMS = [
47
+ 'response_type',
48
+ 'client_id',
49
+ 'redirect_uri',
50
+ 'state',
51
+ 'code_challenge',
52
+ 'code_challenge_method',
53
+ ] as const;
54
+
55
+ export function buildAuthorizationUrl(p: AuthorizationUrlParams): string {
56
+ const params: Record<string, string> = {
57
+ response_type: 'code',
58
+ client_id: p.clientId,
59
+ redirect_uri: p.redirectUri,
60
+ state: p.state,
61
+ };
62
+ if (p.scopes && p.scopes.length > 0) {
63
+ params.scope = p.scopes.join(' ');
64
+ }
65
+ if (p.codeChallenge) {
66
+ params.code_challenge = p.codeChallenge;
67
+ params.code_challenge_method = 'S256';
68
+ }
69
+ const reserved: readonly string[] = RESERVED_AUTHORIZATION_PARAMS;
70
+ for (const [key, value] of Object.entries(p.additionalParameters ?? {})) {
71
+ if (reserved.includes(key)) {
72
+ warnOnce(
73
+ 'oauthSignIn',
74
+ `additionalParameters.${key}`,
75
+ `additionalParameters.${key} is ignored: the library sets it (use the matching option instead).`
76
+ );
77
+ continue;
78
+ }
79
+ params[key] = value;
80
+ }
81
+
82
+ const separator = p.authorizationEndpoint.includes('?') ? '&' : '?';
83
+ return `${p.authorizationEndpoint}${separator}${encodeQuery(params)}`;
84
+ }
85
+
86
+ /**
87
+ * Reads the query and fragment parameters of a redirect url.
88
+ */
89
+ export function parseRedirectParams(url: string): Record<string, string> {
90
+ const params: Record<string, string> = {};
91
+ const queryStart = url.indexOf('?');
92
+ const hashStart = url.indexOf('#');
93
+ const parts: string[] = [];
94
+ if (queryStart >= 0) {
95
+ parts.push(
96
+ url.slice(queryStart + 1, hashStart > queryStart ? hashStart : undefined)
97
+ );
98
+ }
99
+ if (hashStart >= 0) {
100
+ parts.push(url.slice(hashStart + 1));
101
+ }
102
+ for (const part of parts) {
103
+ for (const pair of part.split('&')) {
104
+ if (!pair) continue;
105
+ const eq = pair.indexOf('=');
106
+ const key = decodeComponent(eq >= 0 ? pair.slice(0, eq) : pair);
107
+ params[key] = eq >= 0 ? decodeComponent(pair.slice(eq + 1)) : '';
108
+ }
109
+ }
110
+ return params;
111
+ }
112
+
113
+ export function getUrlScheme(url: string): string {
114
+ const index = url.indexOf(':');
115
+ if (index <= 0) {
116
+ throw createCredentialError(
117
+ CredentialError.CONFIGURATION_ERROR,
118
+ `Invalid redirect uri: ${url}`
119
+ );
120
+ }
121
+ return url.slice(0, index);
122
+ }
123
+
124
+ export function buildTokenRequestBody(p: {
125
+ code: string;
126
+ clientId: string;
127
+ redirectUri: string;
128
+ codeVerifier: string | null;
129
+ }): string {
130
+ const params: Record<string, string> = {
131
+ grant_type: 'authorization_code',
132
+ code: p.code,
133
+ client_id: p.clientId,
134
+ redirect_uri: p.redirectUri,
135
+ };
136
+ if (p.codeVerifier) {
137
+ params.code_verifier = p.codeVerifier;
138
+ }
139
+ return encodeQuery(params);
140
+ }
@@ -0,0 +1,121 @@
1
+ import type { GoogleClientId, GoogleSignInOptions } from './types';
2
+
3
+ type OS = 'ios' | 'android' | string;
4
+
5
+ /**
6
+ * Picks the client id the native module of `os` expects.
7
+ *
8
+ * A string is passed as is. With `{ iosClientId, webClientId }`, iOS gets `iosClientId` (or '' so
9
+ * the native side reads `GIDClientID` from Info.plist) and `webClientId` as the default
10
+ * `serverClientId`; Android gets `webClientId` (or '', which rejects with `CONFIGURATION_ERROR`).
11
+ */
12
+ export function resolveGoogleClientId<T extends GoogleSignInOptions>(
13
+ os: OS,
14
+ clientId: GoogleClientId | null | undefined,
15
+ options: T
16
+ ): { clientId: string; options: T } {
17
+ if (typeof clientId === 'string') {
18
+ return { clientId, options };
19
+ }
20
+ const ids = clientId ?? {};
21
+ if (os === 'ios') {
22
+ const resolved = { ...options };
23
+ if (resolved.serverClientId === undefined && ids.webClientId) {
24
+ resolved.serverClientId = ids.webClientId;
25
+ }
26
+ return { clientId: ids.iosClientId ?? '', options: resolved };
27
+ }
28
+ return { clientId: ids.webClientId ?? '', options };
29
+ }
30
+
31
+ /**
32
+ * Splits the `(clientId?, options?)` arguments of the Google methods, which also accept
33
+ * `(options)`: an object with `iosClientId` or `webClientId` is the client id, any other object is
34
+ * the options. Without a client id the native side reads it from the app config (Info.plist,
35
+ * AndroidManifest meta-data or the Firebase files).
36
+ */
37
+ export function splitGoogleArgs<T extends object>(
38
+ first: GoogleClientId | T | null | undefined,
39
+ second: T | undefined
40
+ ): { clientId: GoogleClientId | undefined; options: T | undefined } {
41
+ if (first === null || first === undefined) {
42
+ return { clientId: undefined, options: second };
43
+ }
44
+ if (
45
+ typeof first === 'string' ||
46
+ 'iosClientId' in first ||
47
+ 'webClientId' in first
48
+ ) {
49
+ return { clientId: first as GoogleClientId, options: second };
50
+ }
51
+ return { clientId: undefined, options: first as T };
52
+ }
53
+
54
+ /**
55
+ * Message thrown when the native module is not in the binary: Expo Go (which only ships the Expo
56
+ * SDK native modules) or an app that was not rebuilt after installing the library.
57
+ */
58
+ export function missingNativeModuleMessage(): string {
59
+ const expo = (globalThis as { expo?: { modules?: Record<string, unknown> } })
60
+ .expo;
61
+ if (expo?.modules?.ExpoGo) {
62
+ return (
63
+ 'react-native-oauth-essentials does not work in Expo Go, which only contains the Expo SDK ' +
64
+ 'native code. Create a development build instead: `npx expo run:ios` / `npx expo run:android` ' +
65
+ 'or `eas build --profile development`.'
66
+ );
67
+ }
68
+ return (
69
+ 'react-native-oauth-essentials: the native module "OauthEssentials" was not found. Rebuild the ' +
70
+ 'app after installing the library (run `pod install` in ios/ and build again; reloading Metro is ' +
71
+ 'not enough) and make sure the New Architecture is enabled. With Expo, use a development build ' +
72
+ '(Expo Go is not supported).'
73
+ );
74
+ }
75
+
76
+ const warned = new Set<string>();
77
+
78
+ /**
79
+ * In development, warns once per method and option that an option set by the app has no effect on
80
+ * the current platform. `options` must be the options as passed by the app (before defaults).
81
+ */
82
+ export function warnIgnored(
83
+ method: string,
84
+ platform: string,
85
+ options: object | null | undefined,
86
+ keys: readonly string[]
87
+ ): void {
88
+ if (!__DEV__ || !options) {
89
+ return;
90
+ }
91
+ for (const key of keys) {
92
+ if ((options as Record<string, unknown>)[key] !== undefined) {
93
+ warnOnce(
94
+ method,
95
+ key,
96
+ `"${key}" is ignored on ${platformName(platform)}.`
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ function platformName(platform: string): string {
103
+ if (platform === 'ios') return 'iOS';
104
+ if (platform === 'android') return 'Android';
105
+ return platform;
106
+ }
107
+
108
+ /**
109
+ * In development, logs `message` once per method and key.
110
+ */
111
+ export function warnOnce(method: string, key: string, message: string): void {
112
+ if (!__DEV__) {
113
+ return;
114
+ }
115
+ const id = `${method}:${key}`;
116
+ if (warned.has(id)) {
117
+ return;
118
+ }
119
+ warned.add(id);
120
+ console.warn(`[react-native-oauth-essentials] ${method}: ${message}`);
121
+ }
package/src/types.ts CHANGED
@@ -1,12 +1,142 @@
1
1
  export type GoogleSignInOptions = {
2
+ /**
3
+ * Only show accounts that already signed in to the app. [Android only]
4
+ */
2
5
  authorizedAccounts?: boolean;
6
+ /**
7
+ * Automatically sign in when a single authorized account is available. [Android only]
8
+ */
3
9
  autoSelectEnabled?: boolean;
10
+ /**
11
+ * Android UI flow. `bottomSheet` (default) shows the Credential Manager sheet and, unless
12
+ * `authorizedAccounts` is set, falls back to `button` when no credential is available
13
+ * (no account on the device or the sheet is in cooldown after being dismissed).
14
+ * `button` always shows the "Sign in with Google" flow, which is what a sign-in button tap
15
+ * should use. [Android only]
16
+ */
17
+ mode?: 'bottomSheet' | 'button';
18
+ /**
19
+ * Nonce embedded as-is in the returned idToken, so your backend can prevent replay attacks.
20
+ * Ignored by `googleSilentSignIn` on iOS.
21
+ */
22
+ nonce?: string;
23
+ /**
24
+ * Email to preselect in the Google page. [iOS only]
25
+ */
26
+ hint?: string;
27
+ /**
28
+ * Restricts sign in to a Google Workspace domain. On Android it only applies to the
29
+ * "Sign in with Google" (`button`) flow and to the authorization step, not to the bottom sheet.
30
+ */
31
+ hostedDomain?: string;
32
+ /**
33
+ * Additional OAuth scopes the user must grant.
34
+ * iOS: requested during sign in; `accessToken` and `grantedScopes` are always returned.
35
+ * Android: requested in a second step (Authorization API, may show a consent screen); without
36
+ * `scopes`/`offlineAccess`, `accessToken` and `grantedScopes` are null.
37
+ */
38
+ scopes?: string[];
39
+ /**
40
+ * Android: requests a `serverAuthCode` (for `serverClientId`, or the main client id) your backend
41
+ * can exchange for refresh tokens. iOS: no effect, `serverAuthCode` is returned whenever
42
+ * `serverClientId` is set.
43
+ */
44
+ offlineAccess?: boolean;
45
+ /**
46
+ * iOS: web (server) client id. Makes it the idToken audience and returns a `serverAuthCode`.
47
+ * Android: client id the `serverAuthCode` is issued for (defaults to the main client id).
48
+ */
49
+ serverClientId?: string;
4
50
  };
5
51
 
52
+ /**
53
+ * Google OAuth client ids per platform, all from the same Google Cloud project.
54
+ *
55
+ * Both are optional: a missing id is read from the app config, which the Expo plugin writes (from
56
+ * its options or the Firebase files).
57
+ */
58
+ export type GoogleClientIds = {
59
+ /**
60
+ * iOS client id (created with the iOS bundle id). Used on iOS only. When missing, the `GIDClientID`
61
+ * key of Info.plist is used, then `CLIENT_ID` of GoogleService-Info.plist.
62
+ */
63
+ iosClientId?: string;
64
+ /**
65
+ * Web client id. Android: the client id passed to Credential Manager; when missing, the
66
+ * `com.oauthessentials.GOOGLE_WEB_CLIENT_ID` meta-data of AndroidManifest is used, then the
67
+ * `default_web_client_id` string generated by the Firebase `google-services` Gradle plugin.
68
+ * iOS: the default `serverClientId` (idToken audience); when missing, the `GIDServerClientID` key
69
+ * of Info.plist is used.
70
+ */
71
+ webClientId?: string;
72
+ };
73
+
74
+ /**
75
+ * A {@link GoogleClientIds} object, or a single client id string, used as is on both platforms
76
+ * (it must be the iOS client id on iOS and the web client id on Android).
77
+ */
78
+ export type GoogleClientId = string | GoogleClientIds;
79
+
80
+ export type AppleSignInOptions = {
81
+ /**
82
+ * Nonce passed as-is to Apple and embedded in the idToken. Firebase and Supabase expect the
83
+ * SHA-256 of the raw nonce here. [iOS only, on Android put it in your `androidWebUrl`]
84
+ */
85
+ nonce?: string;
86
+ /**
87
+ * Opaque value returned in the credential. [iOS only]
88
+ */
89
+ state?: string;
90
+ };
91
+
92
+ export type HybridSignInOptions = GoogleSignInOptions;
93
+
6
94
  export type PasskeyOptions = {
95
+ /**
96
+ * WebAuthn options JSON from your server: `PublicKeyCredentialCreationOptions` for
97
+ * `createPassKey`, `PublicKeyCredentialRequestOptions` for `getPassKey`.
98
+ */
7
99
  requestJson: string;
8
- preferImmediatelyAvailableCredentials?: boolean; // [Android only]
9
- isConditional?: boolean; // [Android only]
100
+ /**
101
+ * Only offer credentials already available on the device. [Android only, iOS modal requests
102
+ * always behave this way]
103
+ */
104
+ preferImmediatelyAvailableCredentials?: boolean;
105
+ /**
106
+ * `autofill` shows passkeys in the QuickType bar of a text field with
107
+ * `textContentType="username"` instead of a modal sheet. [iOS 16+ only]
108
+ */
109
+ mediation?: 'modal' | 'autofill';
110
+ };
111
+
112
+ export type OAuthSignInOptions = {
113
+ authorizationEndpoint: string;
114
+ clientId: string;
115
+ /**
116
+ * Defaults to `OAUTH_REDIRECT_URI` (`<bundle or application id>://oauth/callback`).
117
+ */
118
+ redirectUri?: string;
119
+ scopes?: string[];
120
+ /**
121
+ * Extra authorization request parameters (e.g. `prompt`, `login_hint`, `nonce`). They may
122
+ * override `scope`, but `response_type`, `client_id`, `redirect_uri`, `state`, `code_challenge`
123
+ * and `code_challenge_method` are set by the library and ignored here (with a warning in
124
+ * development).
125
+ */
126
+ additionalParameters?: Record<string, string>;
127
+ /**
128
+ * When set, the authorization code is exchanged for tokens on the device.
129
+ * Leave it out to exchange the code on your backend (recommended for confidential clients).
130
+ */
131
+ tokenEndpoint?: string;
132
+ /**
133
+ * Defaults to true.
134
+ */
135
+ usePKCE?: boolean;
136
+ /**
137
+ * Do not share cookies with the browser. [iOS only]
138
+ */
139
+ prefersEphemeralSession?: boolean;
10
140
  };
11
141
 
12
142
  export type PassKeyCredentialResult = {
@@ -25,13 +155,20 @@ export type PasswordCredentialResult = {
25
155
  export type GoogleIdCredentialResult = {
26
156
  type: CredentialsType.GOOGLE_ID;
27
157
  data: {
158
+ /** iOS: Google user id (the idToken `sub`). Android: the account email. */
28
159
  id: string;
29
160
  idToken: string;
30
161
  displayName: string;
31
162
  givenName: string;
32
163
  familyName: string;
164
+ email?: string;
165
+ /** iOS: 200px picture url or "". */
33
166
  profilePictureUri?: string | null;
167
+ /** Android only. */
34
168
  phoneNumber?: string | null;
169
+ accessToken?: string | null;
170
+ serverAuthCode?: string | null;
171
+ grantedScopes?: string[] | null;
35
172
  };
36
173
  };
37
174
 
@@ -49,6 +186,7 @@ export type AppleIdCredentialResult = {
49
186
  email: string;
50
187
  fullName: string;
51
188
  likelyReal: boolean;
189
+ state?: string | null;
52
190
  };
53
191
  };
54
192
 
@@ -63,6 +201,30 @@ export type WebAppleIdCredentialResult = {
63
201
  };
64
202
  };
65
203
 
204
+ export type OAuthCredentialResult = {
205
+ type: CredentialsType.OAUTH;
206
+ data: {
207
+ code: string;
208
+ state: string;
209
+ redirectUri: string;
210
+ codeVerifier: string | null;
211
+ /** All parameters received in the redirect url. */
212
+ params: Record<string, string>;
213
+ /** Token endpoint response, only when `tokenEndpoint` is set. */
214
+ tokens: Record<string, unknown> | null;
215
+ };
216
+ };
217
+
218
+ export type CredentialSuccessEvent = {
219
+ type: string;
220
+ data: Object;
221
+ };
222
+
223
+ export type CredentialFailureEvent = {
224
+ code: CredentialErrorCode;
225
+ message: string;
226
+ };
227
+
66
228
  export enum CredentialsType {
67
229
  GOOGLE_ID = 'GOOGLE_ID', // GoogleId credentials type
68
230
  PASSWORD = 'PASSWORD', // Password credentials type
@@ -70,10 +232,46 @@ export enum CredentialsType {
70
232
  WEB_APPLE_ID = 'WEB_APPLE_ID', // Android AppleId credentials type
71
233
  CANCELLED = 'CANCELLED', // type when the user cancels it
72
234
  PASSKEY = 'PASSKEY', // Passkey credential type
235
+ OAUTH = 'OAUTH', // Generic OAuth2/OIDC authorization code
236
+ }
237
+
238
+ export enum AppleIdCredentialState {
239
+ AUTHORIZED = 'AUTHORIZED',
240
+ REVOKED = 'REVOKED',
241
+ NOT_FOUND = 'NOT_FOUND',
242
+ TRANSFERRED = 'TRANSFERRED',
73
243
  }
74
244
 
75
245
  export enum CredentialError {
246
+ /**
247
+ * @deprecated Never thrown: missing Play Services rejects with `NOT_SUPPORTED_ERROR`.
248
+ */
76
249
  NO_PLAY_SERVICES_ERROR = 'NO_PLAY_SERVICES_ERROR',
250
+ /** No Android activity, or no iOS view controller attached to a window, to present the UI. */
77
251
  NO_ACTIVITY_ERROR = 'NO_ACTIVITY_ERROR',
252
+ /** The provider failed or returned something unexpected; `message` has the native error. */
78
253
  INVALID_RESULT_ERROR = 'INVALID_RESULT_ERROR',
254
+ /** There is no credential to return (no account / passkey / previous sign in). */
255
+ NO_SIGN_IN_AVAILABLE = 'NO_SIGN_IN_AVAILABLE',
256
+ /** iOS: the same method (or method group) is still waiting for the previous call. */
257
+ SIMULTANEOUS_CALL_ERROR = 'SIMULTANEOUS_CALL_ERROR',
258
+ /** The OS version, missing Play Services or the platform does not support the method. */
259
+ NOT_SUPPORTED_ERROR = 'NOT_SUPPORTED_ERROR',
260
+ /** The browser session could not be opened (app in background, Guided Access, no browser). */
261
+ BROWSER_UNAVAILABLE = 'BROWSER_UNAVAILABLE',
262
+ /**
263
+ * The app is misconfigured: missing Google client id, missing iOS URL scheme (the message has
264
+ * the exact scheme to add) or an invalid `redirectUri`.
265
+ */
266
+ CONFIGURATION_ERROR = 'CONFIGURATION_ERROR',
267
+ /** `oauthSignIn`: the token endpoint request failed (offline, DNS, TLS...). */
268
+ NETWORK_ERROR = 'NETWORK_ERROR',
79
269
  }
270
+
271
+ /** A {@link CredentialError} value, as an enum member or its string. */
272
+ export type CredentialErrorCode = `${CredentialError}`;
273
+
274
+ /** Every promise of the library rejects with an `Error` that has one of these codes. */
275
+ export type OauthEssentialsError<
276
+ C extends CredentialErrorCode = CredentialErrorCode
277
+ > = Error & { code: C };