react-native-nitro-auth 0.1.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/LICENSE +21 -0
- package/README.md +254 -0
- package/android/CMakeLists.txt +37 -0
- package/android/build.gradle +99 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/JniOnLoad.cpp +7 -0
- package/android/src/main/cpp/PlatformAuth+Android.cpp +293 -0
- package/android/src/main/java/com/auth/AuthAdapter.kt +286 -0
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +73 -0
- package/android/src/main/java/com/auth/NitroAuthModule.kt +19 -0
- package/android/src/main/java/com/auth/NitroAuthPackage.kt +16 -0
- package/app.plugin.js +64 -0
- package/cpp/AuthCache.cpp +105 -0
- package/cpp/AuthCache.hpp +20 -0
- package/cpp/HybridAuth.cpp +213 -0
- package/cpp/HybridAuth.hpp +47 -0
- package/cpp/JSONSerializer.hpp +57 -0
- package/cpp/PlatformAuth.hpp +25 -0
- package/ios/AuthAdapter.swift +200 -0
- package/ios/PlatformAuth+iOS.mm +119 -0
- package/lib/commonjs/Auth.nitro.js +6 -0
- package/lib/commonjs/Auth.nitro.js.map +1 -0
- package/lib/commonjs/Auth.web.js +256 -0
- package/lib/commonjs/Auth.web.js.map +1 -0
- package/lib/commonjs/global.d.js +6 -0
- package/lib/commonjs/global.d.js.map +1 -0
- package/lib/commonjs/index.js +52 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/index.web.js +52 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/service.js +9 -0
- package/lib/commonjs/service.js.map +1 -0
- package/lib/commonjs/service.web.js +13 -0
- package/lib/commonjs/service.web.js.map +1 -0
- package/lib/commonjs/ui/social-button.js +103 -0
- package/lib/commonjs/ui/social-button.js.map +1 -0
- package/lib/commonjs/ui/social-button.web.js +102 -0
- package/lib/commonjs/ui/social-button.web.js.map +1 -0
- package/lib/commonjs/use-auth.js +144 -0
- package/lib/commonjs/use-auth.js.map +1 -0
- package/lib/module/Auth.nitro.js +4 -0
- package/lib/module/Auth.nitro.js.map +1 -0
- package/lib/module/Auth.web.js +252 -0
- package/lib/module/Auth.web.js.map +1 -0
- package/lib/module/global.d.js +4 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/index.web.js +7 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/service.js +5 -0
- package/lib/module/service.js.map +1 -0
- package/lib/module/service.web.js +4 -0
- package/lib/module/service.web.js.map +1 -0
- package/lib/module/ui/social-button.js +97 -0
- package/lib/module/ui/social-button.js.map +1 -0
- package/lib/module/ui/social-button.web.js +96 -0
- package/lib/module/ui/social-button.web.js.map +1 -0
- package/lib/module/use-auth.js +140 -0
- package/lib/module/use-auth.js.map +1 -0
- package/lib/typescript/Auth.nitro.d.ts +38 -0
- package/lib/typescript/Auth.nitro.d.ts.map +1 -0
- package/lib/typescript/Auth.web.d.ts +32 -0
- package/lib/typescript/Auth.web.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +5 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/index.web.d.ts +5 -0
- package/lib/typescript/index.web.d.ts.map +1 -0
- package/lib/typescript/service.d.ts +3 -0
- package/lib/typescript/service.d.ts.map +1 -0
- package/lib/typescript/service.web.d.ts +2 -0
- package/lib/typescript/service.web.d.ts.map +1 -0
- package/lib/typescript/ui/social-button.d.ts +17 -0
- package/lib/typescript/ui/social-button.d.ts.map +1 -0
- package/lib/typescript/ui/social-button.web.d.ts +17 -0
- package/lib/typescript/ui/social-button.web.d.ts.map +1 -0
- package/lib/typescript/use-auth.d.ts +15 -0
- package/lib/typescript/use-auth.d.ts.map +1 -0
- package/nitro.json +15 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroAuth+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroAuth+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroAuthOnLoad.cpp +44 -0
- package/nitrogen/generated/android/NitroAuthOnLoad.hpp +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/auth/NitroAuthOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroAuth+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NitroAuthAutolinking.mm +35 -0
- package/nitrogen/generated/ios/NitroAuthAutolinking.swift +12 -0
- package/nitrogen/generated/shared/c++/AuthProvider.hpp +76 -0
- package/nitrogen/generated/shared/c++/AuthTokens.hpp +84 -0
- package/nitrogen/generated/shared/c++/AuthUser.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +30 -0
- package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +85 -0
- package/nitrogen/generated/shared/c++/LoginOptions.hpp +81 -0
- package/package.json +113 -0
- package/react-native-nitro-auth.podspec +40 -0
- package/src/Auth.nitro.ts +50 -0
- package/src/Auth.web.ts +310 -0
- package/src/global.d.ts +38 -0
- package/src/index.ts +4 -0
- package/src/index.web.ts +4 -0
- package/src/service.ts +4 -0
- package/src/service.web.ts +1 -0
- package/src/ui/social-button.tsx +129 -0
- package/src/ui/social-button.web.tsx +128 -0
- package/src/use-auth.ts +157 -0
package/package.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-nitro-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance authentication library for React Native with Google Sign-In and Apple Sign-In support, powered by Nitro Modules (JSI)",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
|
+
"types": "lib/typescript/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"browser": "src/index.web.ts",
|
|
10
|
+
"source": "src/index.ts",
|
|
11
|
+
"app": "app.plugin.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"lib",
|
|
15
|
+
"cpp",
|
|
16
|
+
"android",
|
|
17
|
+
"ios",
|
|
18
|
+
"nitrogen",
|
|
19
|
+
"nitro.json",
|
|
20
|
+
"*.podspec",
|
|
21
|
+
"app.plugin.js",
|
|
22
|
+
"!**/__tests__",
|
|
23
|
+
"!**/__fixtures__",
|
|
24
|
+
"!**/__mocks__",
|
|
25
|
+
"!cpp/core/*Test.cpp",
|
|
26
|
+
"!cpp/build",
|
|
27
|
+
"!android/build",
|
|
28
|
+
"!android/.cxx",
|
|
29
|
+
"!scripts"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"prebuild": "npm run codegen",
|
|
33
|
+
"build": "bob build",
|
|
34
|
+
"clean": "rimraf lib nitrogen/generated",
|
|
35
|
+
"codegen": "nitrogen --logLevel=\"debug\"",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"test": "jest",
|
|
38
|
+
"test:coverage": "jest --coverage",
|
|
39
|
+
"test:cpp": "node scripts/test-cpp.js",
|
|
40
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
41
|
+
"prepack": "node -e \"const fs=require('fs'); fs.copyFileSync('../../README.md','./README.md'); try{fs.copyFileSync('../../LICENSE','./LICENSE')}catch(e){}\"",
|
|
42
|
+
"postpack": "node -e \"const fs=require('fs'); ['./README.md','./LICENSE'].forEach(f=>fs.existsSync(f)&&fs.unlinkSync(f))\""
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react-native",
|
|
46
|
+
"react-native-auth",
|
|
47
|
+
"authentication",
|
|
48
|
+
"google-sign-in",
|
|
49
|
+
"apple-sign-in",
|
|
50
|
+
"social-login",
|
|
51
|
+
"nitro",
|
|
52
|
+
"jsi",
|
|
53
|
+
"nitro-modules",
|
|
54
|
+
"expo",
|
|
55
|
+
"typescript",
|
|
56
|
+
"oauth",
|
|
57
|
+
"auth-provider",
|
|
58
|
+
"sign-in",
|
|
59
|
+
"login"
|
|
60
|
+
],
|
|
61
|
+
"author": {
|
|
62
|
+
"name": "João Paulo C. Marra",
|
|
63
|
+
"url": "https://github.com/JoaoPauloCMarra"
|
|
64
|
+
},
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"homepage": "https://github.com/JoaoPauloCMarra/react-native-nitro-auth#readme",
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "https://github.com/JoaoPauloCMarra/react-native-nitro-auth.git"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/JoaoPauloCMarra/react-native-nitro-auth/issues"
|
|
73
|
+
},
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"registry": "https://registry.npmjs.org/",
|
|
76
|
+
"access": "public"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@expo/config-plugins": "^54.0.4",
|
|
80
|
+
"@react-native/babel-preset": "^0.81.0",
|
|
81
|
+
"@testing-library/react": "^16.3.1",
|
|
82
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
83
|
+
"@testing-library/react-native": "^13.3.3",
|
|
84
|
+
"react": "19.1.0",
|
|
85
|
+
"react-native": "0.81.5",
|
|
86
|
+
"react-native-nitro-modules": "^0.32.0",
|
|
87
|
+
"react-native-web": "^0.21.2"
|
|
88
|
+
},
|
|
89
|
+
"peerDependencies": {
|
|
90
|
+
"react": "*",
|
|
91
|
+
"react-native": ">=0.75.0",
|
|
92
|
+
"react-native-nitro-modules": ">=0.32.0"
|
|
93
|
+
},
|
|
94
|
+
"react-native-builder-bob": {
|
|
95
|
+
"source": "src",
|
|
96
|
+
"output": "lib",
|
|
97
|
+
"targets": [
|
|
98
|
+
[
|
|
99
|
+
"commonjs",
|
|
100
|
+
{
|
|
101
|
+
"esm": true
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"module",
|
|
106
|
+
{
|
|
107
|
+
"esm": true
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"typescript"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-nitro-auth"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "13.0" }
|
|
14
|
+
s.source = { :git => "https://github.com/JoaoPauloCMarra/react-native-nitro-auth.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.swift_version = "5.0"
|
|
17
|
+
|
|
18
|
+
s.source_files = [
|
|
19
|
+
"ios/**/*.{h,m,mm,swift}",
|
|
20
|
+
"cpp/**/*.{h,hpp,c,cpp}"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
s.pod_target_xcconfig = {
|
|
24
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
25
|
+
"CLANG_CXX_LIBRARY" => "libc++",
|
|
26
|
+
"OTHER_LDFLAGS" => "-ObjC",
|
|
27
|
+
"DEFINES_MODULE" => "YES",
|
|
28
|
+
"HEADER_SEARCH_PATHS" => [
|
|
29
|
+
"\"$(PODS_TARGET_SRCROOT)/cpp\"",
|
|
30
|
+
"\"$(PODS_TARGET_SRCROOT)/nitrogen/generated/shared/c++\"",
|
|
31
|
+
"\"$(PODS_TARGET_SRCROOT)/nitrogen/generated/ios\""
|
|
32
|
+
].join(" ")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
s.dependency "React-Core"
|
|
36
|
+
s.dependency "GoogleSignIn"
|
|
37
|
+
|
|
38
|
+
load 'nitrogen/generated/ios/NitroAuth+autolinking.rb'
|
|
39
|
+
add_nitrogen_files(s)
|
|
40
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HybridObject } from "react-native-nitro-modules";
|
|
2
|
+
|
|
3
|
+
export type AuthProvider = "google" | "apple";
|
|
4
|
+
|
|
5
|
+
export type AuthErrorCode =
|
|
6
|
+
| "cancelled"
|
|
7
|
+
| "network_error"
|
|
8
|
+
| "configuration_error"
|
|
9
|
+
| "unsupported_provider"
|
|
10
|
+
| "unknown";
|
|
11
|
+
|
|
12
|
+
export interface LoginOptions {
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
loginHint?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AuthTokens {
|
|
18
|
+
accessToken?: string;
|
|
19
|
+
idToken?: string;
|
|
20
|
+
expirationTime?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthUser {
|
|
24
|
+
provider: AuthProvider;
|
|
25
|
+
email?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
photo?: string;
|
|
28
|
+
idToken?: string;
|
|
29
|
+
accessToken?: string;
|
|
30
|
+
scopes?: string[];
|
|
31
|
+
expirationTime?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Auth extends HybridObject<{ ios: "c++"; android: "c++" }> {
|
|
35
|
+
readonly currentUser: AuthUser | undefined;
|
|
36
|
+
readonly grantedScopes: string[];
|
|
37
|
+
readonly hasPlayServices: boolean;
|
|
38
|
+
|
|
39
|
+
login(provider: AuthProvider, options?: LoginOptions): Promise<void>;
|
|
40
|
+
requestScopes(scopes: string[]): Promise<void>;
|
|
41
|
+
revokeScopes(scopes: string[]): Promise<void>;
|
|
42
|
+
getAccessToken(): Promise<string | undefined>;
|
|
43
|
+
refreshToken(): Promise<AuthTokens>;
|
|
44
|
+
|
|
45
|
+
logout(): void;
|
|
46
|
+
|
|
47
|
+
onAuthStateChanged(
|
|
48
|
+
callback: (user: AuthUser | undefined) => void
|
|
49
|
+
): () => void;
|
|
50
|
+
}
|
package/src/Auth.web.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { Auth, AuthUser, AuthProvider, LoginOptions } from "./Auth.nitro";
|
|
2
|
+
|
|
3
|
+
const CACHE_KEY = "nitro_auth_user";
|
|
4
|
+
const SCOPES_KEY = "nitro_auth_scopes";
|
|
5
|
+
const DEFAULT_SCOPES = ["openid", "email", "profile"];
|
|
6
|
+
|
|
7
|
+
const getConfig = () => {
|
|
8
|
+
try {
|
|
9
|
+
const Constants = require("expo-constants").default;
|
|
10
|
+
return Constants.expoConfig?.extra || {};
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class AuthWeb implements Auth {
|
|
17
|
+
private _currentUser: AuthUser | undefined;
|
|
18
|
+
private _grantedScopes: string[] = [];
|
|
19
|
+
private _listeners: ((user: AuthUser | undefined) => void)[] = [];
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
23
|
+
if (cached) {
|
|
24
|
+
try {
|
|
25
|
+
this._currentUser = JSON.parse(cached);
|
|
26
|
+
} catch {
|
|
27
|
+
localStorage.removeItem(CACHE_KEY);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const scopes = localStorage.getItem(SCOPES_KEY);
|
|
31
|
+
if (scopes) {
|
|
32
|
+
try {
|
|
33
|
+
this._grantedScopes = JSON.parse(scopes);
|
|
34
|
+
} catch {
|
|
35
|
+
localStorage.removeItem(SCOPES_KEY);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get currentUser(): AuthUser | undefined {
|
|
41
|
+
return this._currentUser;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get grantedScopes(): string[] {
|
|
45
|
+
return this._grantedScopes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get hasPlayServices(): boolean {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onAuthStateChanged(
|
|
53
|
+
callback: (user: AuthUser | undefined) => void
|
|
54
|
+
): () => void {
|
|
55
|
+
this._listeners.push(callback);
|
|
56
|
+
callback(this._currentUser);
|
|
57
|
+
return () => {
|
|
58
|
+
this._listeners = this._listeners.filter((l) => l !== callback);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private notify() {
|
|
63
|
+
this._listeners.forEach((l) => l(this._currentUser));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async login(provider: AuthProvider, options?: LoginOptions): Promise<void> {
|
|
67
|
+
const scopes = options?.scopes ?? DEFAULT_SCOPES;
|
|
68
|
+
const loginHint = options?.loginHint;
|
|
69
|
+
try {
|
|
70
|
+
if (provider === "google") {
|
|
71
|
+
return await this.loginGoogle(scopes, loginHint);
|
|
72
|
+
}
|
|
73
|
+
return await this.loginApple();
|
|
74
|
+
} catch (e: unknown) {
|
|
75
|
+
throw this.mapError(e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async requestScopes(scopes: string[]): Promise<void> {
|
|
80
|
+
if (!this._currentUser) {
|
|
81
|
+
throw new Error("No user logged in");
|
|
82
|
+
}
|
|
83
|
+
if (this._currentUser.provider !== "google") {
|
|
84
|
+
throw new Error("Scope management only supported for Google");
|
|
85
|
+
}
|
|
86
|
+
const newScopes = [...new Set([...this._grantedScopes, ...scopes])];
|
|
87
|
+
return this.loginGoogle(newScopes).catch((e) => {
|
|
88
|
+
throw this.mapError(e);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async revokeScopes(scopes: string[]): Promise<void> {
|
|
93
|
+
this._grantedScopes = this._grantedScopes.filter(
|
|
94
|
+
(s) => !scopes.includes(s)
|
|
95
|
+
);
|
|
96
|
+
localStorage.setItem(SCOPES_KEY, JSON.stringify(this._grantedScopes));
|
|
97
|
+
if (this._currentUser) {
|
|
98
|
+
this._currentUser.scopes = this._grantedScopes;
|
|
99
|
+
this.updateUser(this._currentUser);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getAccessToken(): Promise<string | undefined> {
|
|
104
|
+
if (this._currentUser?.expirationTime) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (now + 300000 > this._currentUser.expirationTime) {
|
|
107
|
+
await this.refreshToken();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return this._currentUser?.accessToken;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async refreshToken(): Promise<{ accessToken?: string; idToken?: string }> {
|
|
114
|
+
if (!this._currentUser) {
|
|
115
|
+
throw new Error("No user logged in");
|
|
116
|
+
}
|
|
117
|
+
if (this._currentUser.provider !== "google") {
|
|
118
|
+
throw new Error("Token refresh only supported for Google");
|
|
119
|
+
}
|
|
120
|
+
await this.loginGoogle(
|
|
121
|
+
this._grantedScopes.length > 0 ? this._grantedScopes : DEFAULT_SCOPES
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
accessToken: this._currentUser.accessToken,
|
|
125
|
+
idToken: this._currentUser.idToken,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private mapError(error: unknown): Error {
|
|
130
|
+
const msg = (error as any)?.message?.toLowerCase() ?? "";
|
|
131
|
+
if (msg.includes("cancel") || msg.includes("popup_closed")) {
|
|
132
|
+
return new Error("cancelled");
|
|
133
|
+
}
|
|
134
|
+
if (msg.includes("network")) {
|
|
135
|
+
return new Error("network_error");
|
|
136
|
+
}
|
|
137
|
+
if (msg.includes("client id") || msg.includes("config")) {
|
|
138
|
+
return new Error("configuration_error");
|
|
139
|
+
}
|
|
140
|
+
return error as Error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async loginGoogle(
|
|
144
|
+
scopes: string[],
|
|
145
|
+
loginHint?: string
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const config = getConfig();
|
|
148
|
+
const clientId = config.googleWebClientId;
|
|
149
|
+
|
|
150
|
+
if (!clientId) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"Google Web Client ID not configured. Add 'GOOGLE_WEB_CLIENT_ID' to your .env file."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const redirectUri = window.location.origin;
|
|
158
|
+
const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
159
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
160
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
161
|
+
authUrl.searchParams.set("response_type", "id_token token");
|
|
162
|
+
authUrl.searchParams.set("scope", scopes.join(" "));
|
|
163
|
+
authUrl.searchParams.set("nonce", Math.random().toString(36).slice(2));
|
|
164
|
+
if (loginHint) {
|
|
165
|
+
authUrl.searchParams.set("login_hint", loginHint);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const width = 500;
|
|
169
|
+
const height = 600;
|
|
170
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
171
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
172
|
+
|
|
173
|
+
const popup = window.open(
|
|
174
|
+
authUrl.toString(),
|
|
175
|
+
"google-auth",
|
|
176
|
+
`width=${width},height=${height},left=${left},top=${top}`
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!popup) {
|
|
180
|
+
reject(new Error("Popup blocked. Please allow popups for this site."));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const checkInterval = setInterval(() => {
|
|
185
|
+
try {
|
|
186
|
+
if (popup.closed) {
|
|
187
|
+
clearInterval(checkInterval);
|
|
188
|
+
reject(new Error("cancelled"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const url = popup.location.href;
|
|
193
|
+
if (url.startsWith(redirectUri)) {
|
|
194
|
+
clearInterval(checkInterval);
|
|
195
|
+
popup.close();
|
|
196
|
+
|
|
197
|
+
const hash = new URL(url).hash.slice(1);
|
|
198
|
+
const params = new URLSearchParams(hash);
|
|
199
|
+
const idToken = params.get("id_token");
|
|
200
|
+
const accessToken = params.get("access_token");
|
|
201
|
+
const expiresIn = params.get("expires_in");
|
|
202
|
+
|
|
203
|
+
if (idToken) {
|
|
204
|
+
this._grantedScopes = scopes;
|
|
205
|
+
localStorage.setItem(SCOPES_KEY, JSON.stringify(scopes));
|
|
206
|
+
|
|
207
|
+
const user: AuthUser = {
|
|
208
|
+
provider: "google",
|
|
209
|
+
idToken,
|
|
210
|
+
accessToken: accessToken ?? undefined,
|
|
211
|
+
scopes,
|
|
212
|
+
expirationTime: expiresIn
|
|
213
|
+
? Date.now() + parseInt(expiresIn) * 1000
|
|
214
|
+
: undefined,
|
|
215
|
+
...this.decodeGoogleJwt(idToken),
|
|
216
|
+
};
|
|
217
|
+
this.updateUser(user);
|
|
218
|
+
resolve();
|
|
219
|
+
} else {
|
|
220
|
+
reject(new Error("No id_token in response"));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
}, 100);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private decodeGoogleJwt(token: string): Partial<AuthUser> {
|
|
229
|
+
try {
|
|
230
|
+
const payload = token.split(".")[1];
|
|
231
|
+
const decoded = JSON.parse(atob(payload));
|
|
232
|
+
return {
|
|
233
|
+
email: decoded.email,
|
|
234
|
+
name: decoded.name,
|
|
235
|
+
photo: decoded.picture,
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async loginApple(): Promise<void> {
|
|
243
|
+
const config = getConfig();
|
|
244
|
+
const clientId = config.appleWebClientId;
|
|
245
|
+
|
|
246
|
+
if (!clientId) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const script = document.createElement("script");
|
|
254
|
+
script.src =
|
|
255
|
+
"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
|
|
256
|
+
script.async = true;
|
|
257
|
+
script.onload = () => {
|
|
258
|
+
if (!window.AppleID) {
|
|
259
|
+
reject(new Error("Apple SDK not loaded"));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
window.AppleID.auth.init({
|
|
263
|
+
clientId,
|
|
264
|
+
scope: "name email",
|
|
265
|
+
redirectURI: window.location.origin,
|
|
266
|
+
usePopup: true,
|
|
267
|
+
});
|
|
268
|
+
window.AppleID.auth
|
|
269
|
+
.signIn()
|
|
270
|
+
.then((response: any) => {
|
|
271
|
+
const user: AuthUser = {
|
|
272
|
+
provider: "apple",
|
|
273
|
+
idToken: response.authorization.id_token,
|
|
274
|
+
email: response.user?.email,
|
|
275
|
+
name: response.user?.name
|
|
276
|
+
? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
|
|
277
|
+
: undefined,
|
|
278
|
+
};
|
|
279
|
+
this.updateUser(user);
|
|
280
|
+
resolve();
|
|
281
|
+
})
|
|
282
|
+
.catch((e: any) => reject(this.mapError(e)));
|
|
283
|
+
};
|
|
284
|
+
script.onerror = () => reject(new Error("Failed to load Apple SDK"));
|
|
285
|
+
document.head.appendChild(script);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
logout(): void {
|
|
290
|
+
this._currentUser = undefined;
|
|
291
|
+
this._grantedScopes = [];
|
|
292
|
+
localStorage.removeItem(CACHE_KEY);
|
|
293
|
+
localStorage.removeItem(SCOPES_KEY);
|
|
294
|
+
this.notify();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private updateUser(user: AuthUser) {
|
|
298
|
+
this._currentUser = user;
|
|
299
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(user));
|
|
300
|
+
this.notify();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
name = "Auth";
|
|
304
|
+
dispose() {}
|
|
305
|
+
equals(other: any) {
|
|
306
|
+
return other === this;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const AuthModule = new AuthWeb();
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
google?: {
|
|
4
|
+
accounts: {
|
|
5
|
+
id: {
|
|
6
|
+
initialize: (config: {
|
|
7
|
+
client_id: string;
|
|
8
|
+
callback: (response: { credential: string }) => void;
|
|
9
|
+
}) => void;
|
|
10
|
+
prompt: () => void;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
AppleID?: {
|
|
15
|
+
auth: {
|
|
16
|
+
init: (config: {
|
|
17
|
+
clientId: string;
|
|
18
|
+
scope: string;
|
|
19
|
+
redirectURI: string;
|
|
20
|
+
usePopup: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
signIn: () => Promise<{
|
|
23
|
+
authorization: { id_token: string };
|
|
24
|
+
user?: {
|
|
25
|
+
email?: string;
|
|
26
|
+
name?: {
|
|
27
|
+
firstName?: string;
|
|
28
|
+
lastName?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {};
|
|
38
|
+
|
package/src/index.ts
ADDED
package/src/index.web.ts
ADDED
package/src/service.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AuthModule as AuthService } from "./Auth.web";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
View,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
TextStyle,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { NitroModules } from "react-native-nitro-modules";
|
|
12
|
+
import type { Auth, AuthProvider, AuthUser } from "../Auth.nitro";
|
|
13
|
+
|
|
14
|
+
interface SocialButtonProps {
|
|
15
|
+
provider: AuthProvider;
|
|
16
|
+
variant?: "primary" | "outline" | "white" | "black";
|
|
17
|
+
borderRadius?: number;
|
|
18
|
+
style?: ViewStyle;
|
|
19
|
+
textStyle?: TextStyle;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
onSuccess?: (user: AuthUser) => void;
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
23
|
+
onPress?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function performLogin(provider: AuthProvider): Promise<AuthUser | null> {
|
|
27
|
+
const auth = NitroModules.createHybridObject<Auth>("Auth");
|
|
28
|
+
await auth.login(provider);
|
|
29
|
+
return auth.currentUser ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SocialButton: React.FC<SocialButtonProps> = ({
|
|
33
|
+
provider,
|
|
34
|
+
variant = "primary",
|
|
35
|
+
borderRadius = 8,
|
|
36
|
+
style,
|
|
37
|
+
textStyle,
|
|
38
|
+
disabled,
|
|
39
|
+
onSuccess,
|
|
40
|
+
onError,
|
|
41
|
+
onPress,
|
|
42
|
+
}) => {
|
|
43
|
+
const [loading, setLoading] = useState(false);
|
|
44
|
+
|
|
45
|
+
const handleLogin = () => {
|
|
46
|
+
if (loading || disabled) return;
|
|
47
|
+
if (onPress) {
|
|
48
|
+
onPress();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setLoading(true);
|
|
52
|
+
performLogin(provider)
|
|
53
|
+
.then((user) => {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
if (user) onSuccess?.(user);
|
|
56
|
+
})
|
|
57
|
+
.catch((e) => {
|
|
58
|
+
setLoading(false);
|
|
59
|
+
onError?.(e);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isGoogle = provider === "google";
|
|
64
|
+
const isDisabled = loading || disabled;
|
|
65
|
+
|
|
66
|
+
const getBackgroundColor = () => {
|
|
67
|
+
if (isDisabled) return "#CCCCCC";
|
|
68
|
+
if (variant === "black") return "#000000";
|
|
69
|
+
if (variant === "white") return "#FFFFFF";
|
|
70
|
+
if (variant === "outline") return "transparent";
|
|
71
|
+
return isGoogle ? "#4285F4" : "#000000";
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getTextColor = () => {
|
|
75
|
+
if (variant === "white" || variant === "outline") return "#000000";
|
|
76
|
+
return "#FFFFFF";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getBorderColor = () => {
|
|
80
|
+
if (variant === "outline") return "#DDDDDD";
|
|
81
|
+
return "transparent";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Pressable
|
|
86
|
+
style={[
|
|
87
|
+
styles.button,
|
|
88
|
+
{
|
|
89
|
+
backgroundColor: getBackgroundColor(),
|
|
90
|
+
borderRadius,
|
|
91
|
+
borderColor: getBorderColor(),
|
|
92
|
+
borderWidth: variant === "outline" ? 1 : 0,
|
|
93
|
+
},
|
|
94
|
+
style,
|
|
95
|
+
]}
|
|
96
|
+
onPress={handleLogin}
|
|
97
|
+
disabled={isDisabled}
|
|
98
|
+
>
|
|
99
|
+
<View style={styles.content}>
|
|
100
|
+
{loading ? (
|
|
101
|
+
<ActivityIndicator size="small" color={getTextColor()} />
|
|
102
|
+
) : (
|
|
103
|
+
<Text style={[styles.text, { color: getTextColor() }, textStyle]}>
|
|
104
|
+
Sign in with {provider.charAt(0).toUpperCase() + provider.slice(1)}
|
|
105
|
+
</Text>
|
|
106
|
+
)}
|
|
107
|
+
</View>
|
|
108
|
+
</Pressable>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
button: {
|
|
114
|
+
paddingVertical: 12,
|
|
115
|
+
paddingHorizontal: 16,
|
|
116
|
+
minHeight: 48,
|
|
117
|
+
justifyContent: "center",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
width: "100%",
|
|
120
|
+
},
|
|
121
|
+
content: {
|
|
122
|
+
flexDirection: "row",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
},
|
|
125
|
+
text: {
|
|
126
|
+
fontSize: 16,
|
|
127
|
+
fontWeight: "600",
|
|
128
|
+
},
|
|
129
|
+
});
|