@thoughtbot/react-native-social-auth 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -4,6 +4,8 @@ Drop-in Google Sign-In for React Native using Android's Credential Manager and t
4
4
 
5
5
  **Platform support:** ✅ Android · ✅ iOS
6
6
 
7
+ > ⚠️ **Early development.** This package is pre-1.0 and under active development. The public API — configuration options, method signatures, `GoogleSignInButton` props, and error codes — may change between minor versions without a deprecation cycle. If you need stability, pin the exact version in `package.json` (`"@thoughtbot/react-native-social-auth": "0.x.y"`, not `"^0.x.y"`) and check the [CHANGELOG](CHANGELOG.md) before upgrading. We aim for a stable `1.0.0` once the API has been battle-tested.
8
+
7
9
  ## Features
8
10
 
9
11
  - Android **Credential Manager** and iOS **GoogleSignIn-iOS SDK** integration, both with auto-sign-in + interactive fallback
@@ -65,6 +67,8 @@ The Web client ID is what your code references for the ID-token audience; each p
65
67
 
66
68
  In addition to the Cloud Console step above, the host app needs two iOS-specific changes.
67
69
 
70
+ > **Using Expo?** Skip the manual `Info.plist` and `AppDelegate` edits below — [our config plugin](#expo-config-plugin) handles them during `expo prebuild`. Bare React Native CLI users continue with the manual steps in this section.
71
+
68
72
  ### 1. Register the OAuth URL scheme
69
73
 
70
74
  Google routes the sign-in callback back into your app via a custom URL scheme. Add the reversed iOS Client ID to `Info.plist`:
@@ -85,29 +89,30 @@ If you use **Expo**, declare it in `app.json` under `expo.ios.infoPlist.CFBundle
85
89
 
86
90
  ### 2. Forward incoming URLs to the SDK
87
91
 
88
- In your `AppDelegate`, forward `application(_:open:options:)` to `GoogleSignIn.handleURL(_:)`.
92
+ In your `AppDelegate`, forward `application(_:open:options:)` to `GIDSignIn.sharedInstance.handle(_:)`. Importing `GoogleSignIn` here pulls in the official GoogleSignIn-iOS SDK module (already a transitive dependency of this package).
89
93
 
90
94
  **Swift:**
91
95
  ```swift
92
- import react_native_social_auth
96
+ import GoogleSignIn
93
97
 
94
- func application(
98
+ @objc
99
+ public func application(
95
100
  _ app: UIApplication,
96
101
  open url: URL,
97
102
  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
98
103
  ) -> Bool {
99
- return GoogleSignIn.handleURL(url)
104
+ return GIDSignIn.sharedInstance.handle(url)
100
105
  }
101
106
  ```
102
107
 
103
108
  **Objective-C:**
104
109
  ```objc
105
- #import <react_native_social_auth/GoogleSignIn.h>
110
+ #import <GoogleSignIn/GoogleSignIn.h>
106
111
 
107
112
  - (BOOL)application:(UIApplication *)app
108
113
  openURL:(NSURL *)url
109
114
  options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
110
- return [GoogleSignIn handleURL:url];
115
+ return [[GIDSignIn sharedInstance] handleURL:url];
111
116
  }
112
117
  ```
113
118
 
@@ -124,6 +129,56 @@ GoogleSignIn.configure({
124
129
 
125
130
  Finally, run `cd ios && pod install` after installing the package.
126
131
 
132
+ ## Expo config plugin
133
+
134
+ This package ships an Expo config plugin so you don't have to hand-edit `Info.plist` or `AppDelegate` in Expo projects. **Both React Native CLI and Expo projects are supported** — pick the setup section that matches your project.
135
+
136
+ > **Heads up:** Expo Go cannot ship third-party native modules. You must use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (via `expo-dev-client` and EAS Build) or the bare workflow.
137
+
138
+ ### Install
139
+
140
+ ```sh
141
+ npx expo install @thoughtbot/react-native-social-auth react-native-svg
142
+ ```
143
+
144
+ ### Add the plugin
145
+
146
+ In `app.config.ts` (or `app.json`):
147
+
148
+ ```ts
149
+ export default {
150
+ expo: {
151
+ // ...
152
+ plugins: [
153
+ [
154
+ '@thoughtbot/react-native-social-auth',
155
+ {
156
+ iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID,
157
+ },
158
+ ],
159
+ ],
160
+ },
161
+ };
162
+ ```
163
+
164
+ ### Plugin props
165
+
166
+ | Prop | Type | Required for iOS | Description |
167
+ | ------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
168
+ | `iosClientId` | `string` | Yes | Your iOS OAuth Client ID (e.g. `123456-abc.apps.googleusercontent.com`). The plugin reverses it and registers the URL scheme. |
169
+
170
+ Omit `iosClientId` if you only target Android — the plugin becomes a no-op on iOS and logs a warning.
171
+
172
+ ### Regenerate native code
173
+
174
+ ```sh
175
+ npx expo prebuild --clean
176
+ ```
177
+
178
+ This runs the plugin, which writes the reversed iOS Client ID into `Info.plist`'s `CFBundleURLSchemes` and adds the `application(_:open:options:)` URL forwarder to `AppDelegate`. Subsequent prebuilds are idempotent — the plugin won't re-inject if its marker is already present.
179
+
180
+ You still call `GoogleSignIn.configure({ webClientId, iosClientId })` from JS at runtime (the plugin handles the native bits; it doesn't replace `configure()`).
181
+
127
182
  ## Quick start
128
183
 
129
184
  ```tsx
@@ -173,7 +228,7 @@ Stores credentials and options used by subsequent calls.
173
228
  | Field | Type | Required | Description |
174
229
  | ---------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------- |
175
230
  | `webClientId` | `string` | Yes | The **Web application** OAuth Client ID. This is the audience of the issued ID token. |
176
- | `iosClientId` | `string` | No | iOS OAuth Client ID (used once iOS support lands). |
231
+ | `iosClientId` | `string` | No | iOS OAuth Client ID. |
177
232
  | `offlineAccess` | `boolean` | No | Request a server auth code in addition to the ID token. Default `false`. |
178
233
  | `scopes` | `string[]` | No | Additional OAuth scopes beyond the default profile/email. |
179
234
  | `hostedDomain` | `string` | No | Restrict sign-in to a Google Workspace domain. |
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build/withSocialAuth').default;
@@ -0,0 +1,28 @@
1
+ import type { ConfigPlugin } from '@expo/config-plugins';
2
+ export type SocialAuthPluginProps = {
3
+ /**
4
+ * The OAuth 2.0 iOS application client ID from Google Cloud Console
5
+ * (format: `*.apps.googleusercontent.com`). Required to register the
6
+ * URL scheme that GoogleSignIn-iOS uses for its OAuth callback.
7
+ *
8
+ * Omit for Android-only setups — the plugin becomes a no-op on iOS.
9
+ */
10
+ iosClientId?: string;
11
+ };
12
+ /**
13
+ * Compute the reversed iOS Client ID that GoogleSignIn-iOS expects as a
14
+ * `CFBundleURLSchemes` entry.
15
+ *
16
+ * Example:
17
+ * `123-abc.apps.googleusercontent.com`
18
+ * → `com.googleusercontent.apps.123-abc`
19
+ */
20
+ /** @internal — exported for tests only. */
21
+ export declare function reverseClientId(iosClientId: string): string;
22
+ /** @internal — exported for tests only. */
23
+ export declare function injectSwiftURLHandler(contents: string): string;
24
+ /** @internal — exported for tests only. */
25
+ export declare function injectObjCURLHandler(contents: string): string;
26
+ declare const _default: ConfigPlugin<void | SocialAuthPluginProps>;
27
+ export default _default;
28
+ //# sourceMappingURL=withSocialAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withSocialAuth.d.ts","sourceRoot":"","sources":["../../../../plugin/src/withSocialAuth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAIzD,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAKF;;;;;;;GAOG;AACH,2CAA2C;AAC3C,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAS3D;AAyCD,2CAA2C;AAC3C,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAED,2CAA2C;AAC3C,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAyB7D;;AAyBD,wBAA6E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thoughtbot/react-native-social-auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OAuth sign in buttons and methods for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -10,7 +10,8 @@
10
10
  "types": "./lib/typescript/src/index.d.ts",
11
11
  "default": "./lib/module/index.js"
12
12
  },
13
- "./package.json": "./package.json"
13
+ "./package.json": "./package.json",
14
+ "./app.plugin.js": "./app.plugin.js"
14
15
  },
15
16
  "files": [
16
17
  "src",
@@ -18,6 +19,8 @@
18
19
  "android",
19
20
  "ios",
20
21
  "cpp",
22
+ "plugin/build",
23
+ "app.plugin.js",
21
24
  "*.podspec",
22
25
  "react-native.config.js",
23
26
  "!ios/build",
@@ -33,11 +36,12 @@
33
36
  ],
34
37
  "scripts": {
35
38
  "example": "yarn workspace @thoughtbot/react-native-social-auth-example",
36
- "clean": "del-cli lib",
37
- "prepare": "bob build",
39
+ "clean": "del-cli lib plugin/build",
40
+ "prepare": "bob build && yarn build:plugin",
41
+ "build:plugin": "tsc --project plugin/tsconfig.json",
38
42
  "typecheck": "tsc",
39
43
  "test": "jest",
40
- "release": "release-it --only-version",
44
+ "release": "release-it",
41
45
  "lint": "eslint \"**/*.{js,ts,tsx}\""
42
46
  },
43
47
  "keywords": [
@@ -60,19 +64,22 @@
60
64
  },
61
65
  "homepage": "https://github.com/thoughtbot/react-native-social-auth#readme",
62
66
  "publishConfig": {
63
- "registry": "https://registry.npmjs.org/"
67
+ "registry": "https://registry.npmjs.org/",
68
+ "access": "public"
64
69
  },
65
70
  "devDependencies": {
66
71
  "@commitlint/config-conventional": "^20.5.0",
67
72
  "@eslint/compat": "^2.0.3",
68
73
  "@eslint/eslintrc": "^3.3.5",
69
74
  "@eslint/js": "^10.0.1",
75
+ "@expo/config-plugins": "^9",
70
76
  "@jest/globals": "^30.0.0",
71
77
  "@react-native/babel-preset": "0.85.0",
72
78
  "@react-native/eslint-config": "0.85.0",
73
79
  "@react-native/jest-preset": "0.85.0",
74
80
  "@release-it/conventional-changelog": "^10.0.6",
75
81
  "@testing-library/react-native": "^13.3",
82
+ "@types/node": "^26.0.1",
76
83
  "@types/react": "^19.2.0",
77
84
  "@types/react-test-renderer": "^19",
78
85
  "commitlint": "^20.5.0",
@@ -81,6 +88,7 @@
81
88
  "eslint-config-prettier": "^10.1.8",
82
89
  "eslint-plugin-ft-flow": "^3.0.11",
83
90
  "eslint-plugin-prettier": "^5.5.5",
91
+ "expo": "^56.0.12",
84
92
  "jest": "^30.3.0",
85
93
  "lefthook": "^2.1.4",
86
94
  "prettier": "^3.8.1",
@@ -94,10 +102,16 @@
94
102
  "typescript": "^6.0.2"
95
103
  },
96
104
  "peerDependencies": {
105
+ "@expo/config-plugins": ">=9.0.0",
97
106
  "react": "*",
98
107
  "react-native": "*",
99
108
  "react-native-svg": ">=13.0.0"
100
109
  },
110
+ "peerDependenciesMeta": {
111
+ "@expo/config-plugins": {
112
+ "optional": true
113
+ }
114
+ },
101
115
  "workspaces": [
102
116
  "example"
103
117
  ],
@@ -143,19 +157,33 @@
143
157
  "release-it": {
144
158
  "git": {
145
159
  "commitMessage": "chore: release ${version}",
146
- "tagName": "v${version}"
160
+ "tagName": "v${version}",
161
+ "tagAnnotation": "Release ${version}",
162
+ "requireCleanWorkingDir": true,
163
+ "push": true
147
164
  },
148
165
  "npm": {
149
- "publish": true
166
+ "publish": true,
167
+ "skipChecks": true
150
168
  },
151
169
  "github": {
152
- "release": true
170
+ "release": true,
171
+ "releaseName": "v${version}"
172
+ },
173
+ "hooks": {
174
+ "before:init": [
175
+ "yarn lint",
176
+ "yarn typecheck",
177
+ "yarn test --maxWorkers=2"
178
+ ],
179
+ "after:bump": "yarn prepare"
153
180
  },
154
181
  "plugins": {
155
182
  "@release-it/conventional-changelog": {
156
183
  "preset": {
157
184
  "name": "angular"
158
- }
185
+ },
186
+ "infile": "CHANGELOG.md"
159
187
  }
160
188
  }
161
189
  },
@@ -0,0 +1,27 @@
1
+ import type { ConfigPlugin } from '@expo/config-plugins';
2
+ export type SocialAuthPluginProps = {
3
+ /**
4
+ * The OAuth 2.0 iOS application client ID from Google Cloud Console
5
+ * (format: `*.apps.googleusercontent.com`). Required to register the
6
+ * URL scheme that GoogleSignIn-iOS uses for its OAuth callback.
7
+ *
8
+ * Omit for Android-only setups — the plugin becomes a no-op on iOS.
9
+ */
10
+ iosClientId?: string;
11
+ };
12
+ /**
13
+ * Compute the reversed iOS Client ID that GoogleSignIn-iOS expects as a
14
+ * `CFBundleURLSchemes` entry.
15
+ *
16
+ * Example:
17
+ * `123-abc.apps.googleusercontent.com`
18
+ * → `com.googleusercontent.apps.123-abc`
19
+ */
20
+ /** @internal — exported for tests only. */
21
+ export declare function reverseClientId(iosClientId: string): string;
22
+ /** @internal — exported for tests only. */
23
+ export declare function injectSwiftURLHandler(contents: string): string;
24
+ /** @internal — exported for tests only. */
25
+ export declare function injectObjCURLHandler(contents: string): string;
26
+ declare const _default: ConfigPlugin<void | SocialAuthPluginProps>;
27
+ export default _default;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reverseClientId = reverseClientId;
4
+ exports.injectSwiftURLHandler = injectSwiftURLHandler;
5
+ exports.injectObjCURLHandler = injectObjCURLHandler;
6
+ const config_plugins_1 = require("@expo/config-plugins");
7
+ const pkg = require('../../package.json');
8
+ const PLUGIN_NAME = '@thoughtbot/react-native-social-auth';
9
+ const MARKER = '/* @thoughtbot/react-native-social-auth: URL handler */';
10
+ /**
11
+ * Compute the reversed iOS Client ID that GoogleSignIn-iOS expects as a
12
+ * `CFBundleURLSchemes` entry.
13
+ *
14
+ * Example:
15
+ * `123-abc.apps.googleusercontent.com`
16
+ * → `com.googleusercontent.apps.123-abc`
17
+ */
18
+ /** @internal — exported for tests only. */
19
+ function reverseClientId(iosClientId) {
20
+ const suffix = '.apps.googleusercontent.com';
21
+ if (!iosClientId.endsWith(suffix)) {
22
+ throw new Error(`[${PLUGIN_NAME}] iosClientId must end with "${suffix}". Received: "${iosClientId}".`);
23
+ }
24
+ const id = iosClientId.slice(0, -suffix.length);
25
+ return `com.googleusercontent.apps.${id}`;
26
+ }
27
+ const withGoogleSignInURLScheme = (config, { reversedClientId }) => {
28
+ return (0, config_plugins_1.withInfoPlist)(config, (mod) => {
29
+ var _a;
30
+ const urlTypes = ((_a = mod.modResults).CFBundleURLTypes ?? (_a.CFBundleURLTypes = []));
31
+ const alreadyRegistered = urlTypes.some((entry) => (entry.CFBundleURLSchemes ?? []).includes(reversedClientId));
32
+ if (!alreadyRegistered) {
33
+ urlTypes.push({
34
+ CFBundleURLSchemes: [reversedClientId],
35
+ });
36
+ }
37
+ return mod;
38
+ });
39
+ };
40
+ const withGoogleSignInAppDelegate = (config) => {
41
+ return (0, config_plugins_1.withAppDelegate)(config, (mod) => {
42
+ const { language, contents } = mod.modResults;
43
+ if (contents.includes(MARKER)) {
44
+ return mod;
45
+ }
46
+ if (language === 'swift') {
47
+ mod.modResults.contents = injectSwiftURLHandler(contents);
48
+ }
49
+ else if (language === 'objcpp' || language === 'objc') {
50
+ mod.modResults.contents = injectObjCURLHandler(contents);
51
+ }
52
+ else {
53
+ throw new Error(`[${PLUGIN_NAME}] Unknown AppDelegate language "${language}". Expected "swift" or "objcpp".`);
54
+ }
55
+ return mod;
56
+ });
57
+ };
58
+ /** @internal — exported for tests only. */
59
+ function injectSwiftURLHandler(contents) {
60
+ const snippet = `
61
+ ${MARKER}
62
+ @objc
63
+ public func application(
64
+ _ app: UIApplication,
65
+ open url: URL,
66
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
67
+ ) -> Bool {
68
+ return GIDSignIn.sharedInstance.handle(url)
69
+ }
70
+ `;
71
+ const importLine = 'import GoogleSignIn';
72
+ let next = contents;
73
+ if (!next.includes(importLine)) {
74
+ next = next.replace(/(import ExpoModulesCore|import React|import Expo)/, `$1\n${importLine}`);
75
+ }
76
+ // Inject the override before the closing brace of the AppDelegate class.
77
+ const classCloseRegex = /\n\}\s*$/;
78
+ if (!classCloseRegex.test(next)) {
79
+ throw new Error(`[${PLUGIN_NAME}] Could not locate the AppDelegate class closing brace.`);
80
+ }
81
+ return next.replace(classCloseRegex, `\n${snippet}}\n`);
82
+ }
83
+ /** @internal — exported for tests only. */
84
+ function injectObjCURLHandler(contents) {
85
+ const snippet = `
86
+ ${MARKER}
87
+ - (BOOL)application:(UIApplication *)application
88
+ openURL:(NSURL *)url
89
+ options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
90
+ {
91
+ if ([[GIDSignIn sharedInstance] handleURL:url]) { return YES; }
92
+ return [super application:application openURL:url options:options];
93
+ }
94
+ `;
95
+ const importLine = '#import <GoogleSignIn/GoogleSignIn.h>';
96
+ let next = contents;
97
+ if (!next.includes(importLine)) {
98
+ next = next.replace(/(#import "AppDelegate\.h")/, `$1\n${importLine}`);
99
+ }
100
+ const endRegex = /\n@end\s*$/;
101
+ if (!endRegex.test(next)) {
102
+ throw new Error(`[${PLUGIN_NAME}] Could not locate the AppDelegate's @end directive.`);
103
+ }
104
+ return next.replace(endRegex, `\n${snippet}\n@end\n`);
105
+ }
106
+ const withSocialAuth = (config, props) => {
107
+ const iosClientId = props?.iosClientId;
108
+ if (!iosClientId) {
109
+ console.warn(`[${PLUGIN_NAME}] No iosClientId provided — skipping iOS configuration. ` +
110
+ 'Android consumers can ignore this warning; iOS consumers must pass ' +
111
+ '{ iosClientId } in their app config plugin entry.');
112
+ return config;
113
+ }
114
+ const reversedClientId = reverseClientId(iosClientId);
115
+ config = withGoogleSignInURLScheme(config, { reversedClientId });
116
+ config = withGoogleSignInAppDelegate(config);
117
+ return config;
118
+ };
119
+ exports.default = (0, config_plugins_1.createRunOncePlugin)(withSocialAuth, PLUGIN_NAME, pkg.version);