@octane-xplat/auth 0.0.1 → 0.9.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 +37 -2
- package/dist/native/AppleSignInButton.tsrx.js +63 -0
- package/dist/native/GoogleSignInButton.tsrx.js +35 -0
- package/dist/native/apple.js +63 -0
- package/dist/native/google.js +50 -0
- package/dist/native/index.js +5 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/AppleSignInButton.web.tsrx.js +71 -0
- package/dist/web/GoogleSignInButton.web.tsrx.js +47 -0
- package/dist/web/apple.web.js +86 -0
- package/dist/web/google.web.js +130 -0
- package/dist/web/index.js +5 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +77 -10
- package/src/AppleSignInButton.macos.tsrx +36 -0
- package/src/AppleSignInButton.tsrx +46 -0
- package/src/AppleSignInButton.web.tsrx +54 -0
- package/src/GoogleSignInButton.macos.tsrx +7 -0
- package/src/GoogleSignInButton.tsrx +31 -0
- package/src/GoogleSignInButton.web.tsrx +23 -0
- package/src/apple.macos.ts +320 -0
- package/src/apple.ts +95 -0
- package/src/apple.web.ts +114 -0
- package/src/google.macos.ts +18 -0
- package/src/google.ts +61 -0
- package/src/google.web.ts +167 -0
- package/src/index.macos.ts +19 -0
- package/src/index.ts +21 -0
- package/src/index.web.ts +21 -0
- package/src/native-elements.d.ts +10 -0
- package/src/types.ts +147 -0
- package/types/generated/.tsrx-typegen-manifest.json +16 -0
- package/types/generated/AppleSignInButton.d.ts +5 -0
- package/types/generated/AppleSignInButton.web.d.ts +6 -0
- package/types/generated/GoogleSignInButton.d.ts +5 -0
- package/types/generated/GoogleSignInButton.web.d.ts +6 -0
- package/types/generated/apple.d.ts +2 -0
- package/types/generated/apple.web.d.ts +2 -0
- package/types/generated/google.d.ts +2 -0
- package/types/generated/google.web.d.ts +8 -0
- package/types/generated/index.d.ts +5 -0
- package/types/generated/index.web.d.ts +5 -0
- package/types/generated/types.d.ts +133 -0
- package/types/index.macos.d.ts +36 -0
- package/index.d.ts +0 -1
- package/index.js +0 -2
package/src/apple.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Sign in with Apple — native leaf over @nativescript/apple-sign-in.
|
|
2
|
+
// iOS runs ASAuthorizationAppleIDProvider directly (entitlement-driven, no
|
|
3
|
+
// configure step); the plugin's Android module is a stub that reports
|
|
4
|
+
// unsupported, which this leaf surfaces as `supported: false` + an error
|
|
5
|
+
// result rather than a throw.
|
|
6
|
+
import { SignIn } from '@nativescript/apple-sign-in'
|
|
7
|
+
import type {
|
|
8
|
+
AppleAuth,
|
|
9
|
+
AppleAuthConfig,
|
|
10
|
+
AppleCredentialState,
|
|
11
|
+
AppleSignInOptions,
|
|
12
|
+
AuthCredential,
|
|
13
|
+
SignInResult,
|
|
14
|
+
} from './types'
|
|
15
|
+
|
|
16
|
+
function isCancel(error: unknown): boolean {
|
|
17
|
+
// ASAuthorizationErrorCanceled = 1001 (NSError.code via SignInError.native).
|
|
18
|
+
const code = (error as any)?.native?.code ?? (error as any)?.code
|
|
19
|
+
if (code === 1001) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return /cancel/i.test(String((error as any)?.message ?? error))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toCredential(user: any): AuthCredential {
|
|
27
|
+
const fullName = user.fullName
|
|
28
|
+
const name = [fullName?.givenName, fullName?.familyName].filter(Boolean).join(' ') || undefined
|
|
29
|
+
return {
|
|
30
|
+
provider: 'apple',
|
|
31
|
+
idToken: user.identityToken ?? undefined,
|
|
32
|
+
authorizationCode: user.authorizationCode ?? undefined,
|
|
33
|
+
scopes: (user.authorizedScopes ?? []).map((s: string) =>
|
|
34
|
+
s === 'EMAIL' ? 'email' : s === 'FULL_NAME' ? 'name' : s,
|
|
35
|
+
),
|
|
36
|
+
user: {
|
|
37
|
+
id: user.user,
|
|
38
|
+
email: user.email ?? undefined,
|
|
39
|
+
name,
|
|
40
|
+
givenName: fullName?.givenName ?? undefined,
|
|
41
|
+
familyName: fullName?.familyName ?? undefined,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const appleAuth: AppleAuth = {
|
|
47
|
+
supported: SignIn.isSupported(),
|
|
48
|
+
configure(_config: AppleAuthConfig) {
|
|
49
|
+
// No native configuration — the flow is bound to the app id entitlement.
|
|
50
|
+
},
|
|
51
|
+
async signIn(options?: AppleSignInOptions): Promise<SignInResult> {
|
|
52
|
+
if (!SignIn.isSupported()) {
|
|
53
|
+
return {
|
|
54
|
+
status: 'error',
|
|
55
|
+
message: 'Sign in with Apple needs iOS 13+ (unsupported on Android)',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const user = await SignIn.signIn({
|
|
61
|
+
// The plugin SHA-256s `nonce` before binding it into the request.
|
|
62
|
+
scopes: options?.scopes?.map((s) => (s === 'email' ? 'EMAIL' : 'FULL_NAME')) as any,
|
|
63
|
+
useNonce: options?.nonce !== undefined,
|
|
64
|
+
nonce: options?.nonce,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return { status: 'success', credential: toCredential(user) }
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return isCancel(error)
|
|
70
|
+
? { status: 'cancelled' }
|
|
71
|
+
: { status: 'error', message: String((error as any)?.message ?? error) }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async getCredentialState(userId: string): Promise<AppleCredentialState> {
|
|
75
|
+
if (!SignIn.isSupported()) {
|
|
76
|
+
return 'unknown'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const state = await SignIn.getState(userId)
|
|
81
|
+
switch (state) {
|
|
82
|
+
case 'Authorized':
|
|
83
|
+
return 'authorized'
|
|
84
|
+
case 'Revoked':
|
|
85
|
+
return 'revoked'
|
|
86
|
+
case 'Transferred':
|
|
87
|
+
return 'transferred'
|
|
88
|
+
default:
|
|
89
|
+
return 'notFound'
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
return 'unknown'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}
|
package/src/apple.web.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Sign in with Apple — web leaf over Apple's JS API
|
|
2
|
+
// (appleid.cdn.apple.com/appleauth/js/jsapi/AppleID.auth.js), loaded lazily on
|
|
3
|
+
// first use. The flow needs a Services ID (`clientId`) and a registered
|
|
4
|
+
// `redirectURI` via `configure`; `usePopup` (default on) keeps the ceremony in
|
|
5
|
+
// a popup so the app never leaves.
|
|
6
|
+
import type {
|
|
7
|
+
AppleAuth,
|
|
8
|
+
AppleAuthConfig,
|
|
9
|
+
AppleCredentialState,
|
|
10
|
+
AppleSignInOptions,
|
|
11
|
+
AuthCredential,
|
|
12
|
+
SignInResult,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
const APPLE_SDK_URL = 'https://appleid.cdn.apple.com/appleauth/js/jsapi/AppleID.auth.js'
|
|
16
|
+
|
|
17
|
+
let config: AppleAuthConfig = {}
|
|
18
|
+
let requestedScopes: string[] = []
|
|
19
|
+
let sdkPromise: Promise<any> | null = null
|
|
20
|
+
|
|
21
|
+
function loadSdk(): Promise<any> {
|
|
22
|
+
sdkPromise ??= new Promise((resolve, reject) => {
|
|
23
|
+
const script = document.createElement('script')
|
|
24
|
+
script.src = APPLE_SDK_URL
|
|
25
|
+
script.async = true
|
|
26
|
+
script.onload = () => resolve((globalThis as any).AppleID)
|
|
27
|
+
script.onerror = () => {
|
|
28
|
+
sdkPromise = null
|
|
29
|
+
reject(new Error('failed to load the Sign in with Apple JS SDK'))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
document.head.appendChild(script)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return sdkPromise
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeJwt(token: string): Record<string, any> {
|
|
39
|
+
try {
|
|
40
|
+
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
|
|
41
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(part), (c) => c.charCodeAt(0))))
|
|
42
|
+
} catch {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toCredential(response: any): AuthCredential {
|
|
48
|
+
const auth = response?.authorization ?? {}
|
|
49
|
+
const claims = auth.id_token ? decodeJwt(auth.id_token) : {}
|
|
50
|
+
const name =
|
|
51
|
+
[response?.user?.name?.firstName, response?.user?.name?.lastName].filter(Boolean).join(' ') ||
|
|
52
|
+
undefined
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
provider: 'apple',
|
|
56
|
+
idToken: auth.id_token ?? undefined,
|
|
57
|
+
authorizationCode: auth.code ?? undefined,
|
|
58
|
+
scopes: requestedScopes,
|
|
59
|
+
user: {
|
|
60
|
+
id: claims.sub ?? '',
|
|
61
|
+
email: response?.user?.email ?? claims.email ?? undefined,
|
|
62
|
+
name,
|
|
63
|
+
givenName: response?.user?.name?.firstName ?? undefined,
|
|
64
|
+
familyName: response?.user?.name?.lastName ?? undefined,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCancel(error: unknown): boolean {
|
|
70
|
+
const code = (error as any)?.error ?? (error as any)?.message ?? error
|
|
71
|
+
return /popup_closed_by_user|cancel/i.test(String(code))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const appleAuth: AppleAuth = {
|
|
75
|
+
supported: typeof document !== 'undefined',
|
|
76
|
+
configure(next: AppleAuthConfig) {
|
|
77
|
+
config = { ...next }
|
|
78
|
+
},
|
|
79
|
+
async signIn(options?: AppleSignInOptions): Promise<SignInResult> {
|
|
80
|
+
if (!config.clientId || !config.redirectURI) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'error',
|
|
83
|
+
message:
|
|
84
|
+
'appleAuth.configure({ clientId, redirectURI }) is required on web — the Services ID and a registered return URL',
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const sdk = await loadSdk()
|
|
90
|
+
requestedScopes = options?.scopes ?? ['name', 'email']
|
|
91
|
+
sdk.auth.init({
|
|
92
|
+
clientId: config.clientId,
|
|
93
|
+
scope: requestedScopes.join(' '),
|
|
94
|
+
redirectURI: config.redirectURI,
|
|
95
|
+
usePopup: config.usePopup !== false,
|
|
96
|
+
nonce: options?.nonce,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const response = await sdk.auth.signIn()
|
|
100
|
+
return { status: 'success', credential: toCredential(response) }
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return isCancel(error)
|
|
103
|
+
? { status: 'cancelled' }
|
|
104
|
+
: {
|
|
105
|
+
status: 'error',
|
|
106
|
+
message: String((error as any)?.message ?? (error as any)?.error ?? error),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async getCredentialState(): Promise<AppleCredentialState> {
|
|
111
|
+
// The web SDK has no credential-state endpoint.
|
|
112
|
+
return 'unknown'
|
|
113
|
+
},
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Google Sign-In — macOS leaf. The AppKit host has no NativeScript plugin
|
|
2
|
+
// runtime; apps that need Google sign-in on macOS run the hosted ceremony
|
|
3
|
+
// through `@octane-xplat/platform`'s `authSession` instead.
|
|
4
|
+
import type { GoogleAuth, SignInResult } from './types'
|
|
5
|
+
|
|
6
|
+
const UNSUPPORTED: SignInResult = {
|
|
7
|
+
status: 'error',
|
|
8
|
+
message: 'Google Sign-In is unsupported by the AppKit host — use a hosted authSession flow',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const googleAuth: GoogleAuth = {
|
|
12
|
+
supported: false,
|
|
13
|
+
async configure() {},
|
|
14
|
+
async signIn(): Promise<SignInResult> {
|
|
15
|
+
return UNSUPPORTED
|
|
16
|
+
},
|
|
17
|
+
async signOut() {},
|
|
18
|
+
}
|
package/src/google.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Google Sign-In — native leaf over @nativescript/google-signin
|
|
2
|
+
// (GIDSignIn on iOS, Credential Manager/Play services on Android). `configure`
|
|
3
|
+
// forwards to the plugin — clientId is optional on iOS when GIDClientID sits
|
|
4
|
+
// in Info.plist; Android resolves it from google-services resources.
|
|
5
|
+
import { GoogleSignin } from '@nativescript/google-signin'
|
|
6
|
+
import type { AuthCredential, GoogleAuth, GoogleAuthConfig, SignInResult } from './types'
|
|
7
|
+
|
|
8
|
+
function isCancel(error: unknown): boolean {
|
|
9
|
+
const code =
|
|
10
|
+
(error as any)?.native?.code ?? // iOS: kGIDSignInErrorCodeCanceled = -5
|
|
11
|
+
(error as any)?.native?.getStatusCode?.() ?? // Android: ApiException
|
|
12
|
+
(error as any)?.code
|
|
13
|
+
|
|
14
|
+
if (code === -5 || code === 12501 /* SIGN_IN_CANCELLED */) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return /cancel/i.test(String((error as any)?.message ?? error))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toCredential(user: any): AuthCredential {
|
|
22
|
+
return {
|
|
23
|
+
provider: 'google',
|
|
24
|
+
idToken: user.idToken || undefined,
|
|
25
|
+
accessToken: user.accessToken || undefined,
|
|
26
|
+
authorizationCode: user.serverAuthCode || undefined,
|
|
27
|
+
scopes: user.grantedScopes ?? [],
|
|
28
|
+
user: {
|
|
29
|
+
id: user.id,
|
|
30
|
+
email: user.email || undefined,
|
|
31
|
+
name: user.displayName || undefined,
|
|
32
|
+
givenName: user.givenName || undefined,
|
|
33
|
+
familyName: user.familyName || undefined,
|
|
34
|
+
photoUrl: user.photoUrl || undefined,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const googleAuth: GoogleAuth = {
|
|
40
|
+
supported: true,
|
|
41
|
+
async configure(config?: GoogleAuthConfig) {
|
|
42
|
+
await GoogleSignin.configure(config)
|
|
43
|
+
},
|
|
44
|
+
async signIn(): Promise<SignInResult> {
|
|
45
|
+
try {
|
|
46
|
+
const user = await GoogleSignin.signIn()
|
|
47
|
+
return { status: 'success', credential: toCredential(user) }
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return isCancel(error)
|
|
50
|
+
? { status: 'cancelled' }
|
|
51
|
+
: { status: 'error', message: String((error as any)?.message ?? error) }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async signOut() {
|
|
55
|
+
try {
|
|
56
|
+
await GoogleSignin.signOut()
|
|
57
|
+
} catch {
|
|
58
|
+
// A failed sign-out just means the next signIn re-shows the chooser.
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Google Sign-In — web leaf over Google Identity Services
|
|
2
|
+
// (accounts.google.com/gsi/client) in id_token mode, loaded lazily on first
|
|
3
|
+
// use. Headless `signIn` drives One Tap (`google.accounts.id.prompt`); the
|
|
4
|
+
// `GoogleSignInButton` leaf renders the official button via `renderButton`,
|
|
5
|
+
// which is the reliable interactive path — One Tap can be skipped/cancelled
|
|
6
|
+
// without user intent when there is no Google session.
|
|
7
|
+
import type {
|
|
8
|
+
AuthCredential,
|
|
9
|
+
GoogleAuth,
|
|
10
|
+
GoogleAuthConfig,
|
|
11
|
+
GoogleSignInOptions,
|
|
12
|
+
SignInResult,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
const GIS_URL = 'https://accounts.google.com/gsi/client'
|
|
16
|
+
|
|
17
|
+
let config: GoogleAuthConfig = {}
|
|
18
|
+
let sdkPromise: Promise<any> | null = null
|
|
19
|
+
let credentialResolve: ((result: SignInResult) => void) | null = null
|
|
20
|
+
let configuredNonce: string | undefined
|
|
21
|
+
|
|
22
|
+
function loadSdk(): Promise<any> {
|
|
23
|
+
sdkPromise ??= new Promise((resolve, reject) => {
|
|
24
|
+
const script = document.createElement('script')
|
|
25
|
+
script.src = GIS_URL
|
|
26
|
+
script.async = true
|
|
27
|
+
script.onload = () => resolve((globalThis as any).google)
|
|
28
|
+
script.onerror = () => {
|
|
29
|
+
sdkPromise = null
|
|
30
|
+
reject(new Error('failed to load the Google Identity Services SDK'))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.head.appendChild(script)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return sdkPromise
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeJwt(token: string): Record<string, any> {
|
|
40
|
+
try {
|
|
41
|
+
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
|
|
42
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(part), (c) => c.charCodeAt(0))))
|
|
43
|
+
} catch {
|
|
44
|
+
return {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toCredential(response: any): AuthCredential {
|
|
49
|
+
const claims = decodeJwt(response?.credential ?? '')
|
|
50
|
+
return {
|
|
51
|
+
provider: 'google',
|
|
52
|
+
idToken: response?.credential ?? undefined,
|
|
53
|
+
scopes: claims.scope ? String(claims.scope).split(' ') : (config.scopes ?? []),
|
|
54
|
+
user: {
|
|
55
|
+
id: claims.sub ?? '',
|
|
56
|
+
email: claims.email ?? undefined,
|
|
57
|
+
name: claims.name ?? undefined,
|
|
58
|
+
givenName: claims.given_name ?? undefined,
|
|
59
|
+
familyName: claims.family_name ?? undefined,
|
|
60
|
+
photoUrl: claims.picture ?? undefined,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function initialize(google: any) {
|
|
66
|
+
google.accounts.id.initialize({
|
|
67
|
+
client_id: config.clientId,
|
|
68
|
+
callback: (response: any) => {
|
|
69
|
+
const resolve = credentialResolve
|
|
70
|
+
credentialResolve = null
|
|
71
|
+
resolve?.({ status: 'success', credential: toCredential(response) })
|
|
72
|
+
},
|
|
73
|
+
nonce: configuredNonce,
|
|
74
|
+
hosted_domain: config.hostedDomain,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Internal — the button leaf renders the official GIS button into `host`. */
|
|
79
|
+
export async function renderGoogleButton(
|
|
80
|
+
host: HTMLElement,
|
|
81
|
+
options: {
|
|
82
|
+
theme?: 'dark' | 'light' | 'auto'
|
|
83
|
+
variant?: 'standard' | 'wide' | 'icon'
|
|
84
|
+
width?: number
|
|
85
|
+
},
|
|
86
|
+
onResult: (result: SignInResult) => void,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (!config.clientId) {
|
|
89
|
+
onResult({
|
|
90
|
+
status: 'error',
|
|
91
|
+
message: 'googleAuth.configure({ clientId }) is required on web — the OAuth web client id',
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const google = await loadSdk()
|
|
99
|
+
initialize(google)
|
|
100
|
+
google.accounts.id.renderButton(host, {
|
|
101
|
+
type: options.variant === 'icon' ? 'icon' : 'standard',
|
|
102
|
+
theme: options.theme === 'dark' ? 'filled_black' : 'outline',
|
|
103
|
+
size: 'large',
|
|
104
|
+
width: options.width,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// The button's flow resolves through the initialize callback — park the
|
|
108
|
+
// consumer's onResult where the credential callback can find it. One
|
|
109
|
+
// flow at a time, matching the native ceremony.
|
|
110
|
+
credentialResolve = onResult
|
|
111
|
+
} catch (error) {
|
|
112
|
+
onResult({ status: 'error', message: String((error as any)?.message ?? error) })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const googleAuth: GoogleAuth = {
|
|
117
|
+
supported: typeof document !== 'undefined',
|
|
118
|
+
async configure(next?: GoogleAuthConfig) {
|
|
119
|
+
config = { ...next }
|
|
120
|
+
configuredNonce = undefined
|
|
121
|
+
},
|
|
122
|
+
async signIn(options?: GoogleSignInOptions): Promise<SignInResult> {
|
|
123
|
+
if (!config.clientId) {
|
|
124
|
+
return {
|
|
125
|
+
status: 'error',
|
|
126
|
+
message: 'googleAuth.configure({ clientId }) is required on web — the OAuth web client id',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (credentialResolve) {
|
|
131
|
+
return { status: 'error', message: 'a Google sign-in flow is already active' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
configuredNonce = options?.nonce
|
|
135
|
+
try {
|
|
136
|
+
const google = await loadSdk()
|
|
137
|
+
initialize(google)
|
|
138
|
+
return await new Promise<SignInResult>((resolve) => {
|
|
139
|
+
credentialResolve = resolve
|
|
140
|
+
google.accounts.id.prompt((notification: any) => {
|
|
141
|
+
if (
|
|
142
|
+
notification.isNotDisplayed?.() ||
|
|
143
|
+
notification.isSkippedMoment?.() ||
|
|
144
|
+
notification.isDismissedMoment?.()
|
|
145
|
+
) {
|
|
146
|
+
if (credentialResolve === resolve) {
|
|
147
|
+
credentialResolve = null
|
|
148
|
+
resolve({ status: 'cancelled' })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { status: 'error', message: String((error as any)?.message ?? error) }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async signOut() {
|
|
158
|
+
// GIS id-token mode keeps no session — clearing Google's auto-select
|
|
159
|
+
// flag is the web equivalent of the native signOut.
|
|
160
|
+
try {
|
|
161
|
+
const google = await loadSdk()
|
|
162
|
+
google.accounts.id.disableAutoSelect()
|
|
163
|
+
} catch {
|
|
164
|
+
// Nothing to revoke — the credential is stateless.
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { appleAuth } from './apple.macos'
|
|
2
|
+
export { googleAuth } from './google.macos'
|
|
3
|
+
export { AppleSignInButton } from './AppleSignInButton.macos.tsrx'
|
|
4
|
+
export { GoogleSignInButton } from './GoogleSignInButton.macos.tsrx'
|
|
5
|
+
export type {
|
|
6
|
+
AppleAuth,
|
|
7
|
+
AppleAuthConfig,
|
|
8
|
+
AppleCredentialState,
|
|
9
|
+
AppleScope,
|
|
10
|
+
AppleSignInButtonProps,
|
|
11
|
+
AppleSignInOptions,
|
|
12
|
+
AuthCredential,
|
|
13
|
+
AuthUser,
|
|
14
|
+
GoogleAuth,
|
|
15
|
+
GoogleAuthConfig,
|
|
16
|
+
GoogleSignInButtonProps,
|
|
17
|
+
GoogleSignInOptions,
|
|
18
|
+
SignInResult,
|
|
19
|
+
} from './types'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
|
|
2
|
+
// reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
|
|
3
|
+
export { appleAuth } from './apple'
|
|
4
|
+
export { googleAuth } from './google'
|
|
5
|
+
export { AppleSignInButton } from './AppleSignInButton.tsrx'
|
|
6
|
+
export { GoogleSignInButton } from './GoogleSignInButton.tsrx'
|
|
7
|
+
export type {
|
|
8
|
+
AppleAuth,
|
|
9
|
+
AppleAuthConfig,
|
|
10
|
+
AppleCredentialState,
|
|
11
|
+
AppleScope,
|
|
12
|
+
AppleSignInButtonProps,
|
|
13
|
+
AppleSignInOptions,
|
|
14
|
+
AuthCredential,
|
|
15
|
+
AuthUser,
|
|
16
|
+
GoogleAuth,
|
|
17
|
+
GoogleAuthConfig,
|
|
18
|
+
GoogleSignInButtonProps,
|
|
19
|
+
GoogleSignInOptions,
|
|
20
|
+
SignInResult,
|
|
21
|
+
} from './types'
|
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
|
|
2
|
+
// reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
|
|
3
|
+
export { appleAuth } from './apple.web'
|
|
4
|
+
export { googleAuth } from './google.web'
|
|
5
|
+
export { AppleSignInButton } from './AppleSignInButton.web.tsrx'
|
|
6
|
+
export { GoogleSignInButton } from './GoogleSignInButton.web.tsrx'
|
|
7
|
+
export type {
|
|
8
|
+
AppleAuth,
|
|
9
|
+
AppleAuthConfig,
|
|
10
|
+
AppleCredentialState,
|
|
11
|
+
AppleScope,
|
|
12
|
+
AppleSignInButtonProps,
|
|
13
|
+
AppleSignInOptions,
|
|
14
|
+
AuthCredential,
|
|
15
|
+
AuthUser,
|
|
16
|
+
GoogleAuth,
|
|
17
|
+
GoogleAuthConfig,
|
|
18
|
+
GoogleSignInButtonProps,
|
|
19
|
+
GoogleSignInOptions,
|
|
20
|
+
SignInResult,
|
|
21
|
+
} from './types'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SignInButton } from '@nativescript/apple-sign-in'
|
|
2
|
+
import { GoogleSignInButton } from '@nativescript/google-signin'
|
|
3
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
4
|
+
|
|
5
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
6
|
+
interface NativeScriptElements {
|
|
7
|
+
applesigninbutton: Attributes<typeof SignInButton>
|
|
8
|
+
googlesigninbutton: Attributes<typeof GoogleSignInButton>
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Shared contract for @octane-xplat/auth — identical exports across platform
|
|
2
|
+
// leaves (invariant #9). The platform modules (apple.ts / apple.web.ts /
|
|
3
|
+
// apple.macos.ts, google.*) are implementation swaps behind this one API.
|
|
4
|
+
|
|
5
|
+
/** The account the provider authenticated. */
|
|
6
|
+
export interface AuthUser {
|
|
7
|
+
/** Stable subject id — Apple `user`, Google `id` (id-token `sub`). */
|
|
8
|
+
id: string
|
|
9
|
+
email?: string
|
|
10
|
+
/**
|
|
11
|
+
* Best-effort display name. Apple only returns name parts on the first
|
|
12
|
+
* authorization for an account — expect `undefined` on repeat sign-ins.
|
|
13
|
+
*/
|
|
14
|
+
name?: string
|
|
15
|
+
givenName?: string
|
|
16
|
+
familyName?: string
|
|
17
|
+
/** Google only. */
|
|
18
|
+
photoUrl?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The provider credential — hand it to your backend for verification. */
|
|
22
|
+
export interface AuthCredential {
|
|
23
|
+
provider: 'apple' | 'google'
|
|
24
|
+
/** OIDC ID token (JWT). Apple `identityToken`; Google `idToken` / GIS `credential`. */
|
|
25
|
+
idToken?: string
|
|
26
|
+
/**
|
|
27
|
+
* Server-side exchange code — Apple `authorizationCode`; Google
|
|
28
|
+
* `serverAuthCode` (requires `serverClientId` in `GoogleAuthConfig`).
|
|
29
|
+
*/
|
|
30
|
+
authorizationCode?: string
|
|
31
|
+
/** Google only — short-lived API access token. */
|
|
32
|
+
accessToken?: string
|
|
33
|
+
/** Granted scopes where the provider reports them. */
|
|
34
|
+
scopes: string[]
|
|
35
|
+
user: AuthUser
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SignInResult =
|
|
39
|
+
| { status: 'success'; credential: AuthCredential }
|
|
40
|
+
/** The user dismissed the flow, or it ended without a credential. */
|
|
41
|
+
| { status: 'cancelled' }
|
|
42
|
+
| { status: 'error'; message: string }
|
|
43
|
+
|
|
44
|
+
export type AppleScope = 'email' | 'name'
|
|
45
|
+
|
|
46
|
+
export interface AppleAuthConfig {
|
|
47
|
+
/**
|
|
48
|
+
* Web only — the Services ID registered in the Apple developer portal
|
|
49
|
+
* (e.g. `com.example.app.web`). Required on web; ignored natively, where
|
|
50
|
+
* the app id + `com.apple.developer.applesignin` entitlement is the client.
|
|
51
|
+
*/
|
|
52
|
+
clientId?: string
|
|
53
|
+
/** Web only — a return URL registered on the Services ID. */
|
|
54
|
+
redirectURI?: string
|
|
55
|
+
/** Web only — run the flow in a popup instead of a full-page redirect (default true). */
|
|
56
|
+
usePopup?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AppleSignInOptions {
|
|
60
|
+
/** Apple data to request — maps to `EMAIL`/`FULL_NAME` native scopes. */
|
|
61
|
+
scopes?: AppleScope[]
|
|
62
|
+
/** Nonce bound into the credential — pass the value your backend issued. */
|
|
63
|
+
nonce?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type AppleCredentialState =
|
|
67
|
+
| 'authorized'
|
|
68
|
+
| 'revoked'
|
|
69
|
+
| 'notFound'
|
|
70
|
+
| 'transferred'
|
|
71
|
+
/** Anywhere the state check can't run — Android, web. */
|
|
72
|
+
| 'unknown'
|
|
73
|
+
|
|
74
|
+
export interface AppleAuth {
|
|
75
|
+
/**
|
|
76
|
+
* Whether the provider's native SDK can run here — iOS 13+ and the macOS
|
|
77
|
+
* AppKit host (AuthenticationServices via the ObjC bridge; the packaged
|
|
78
|
+
* app needs the applesignin entitlement for the sheet to complete).
|
|
79
|
+
* False on Android. On web it reports whether a browser runtime exists;
|
|
80
|
+
* `configure` still needs a `clientId` before `signIn` will succeed.
|
|
81
|
+
*/
|
|
82
|
+
readonly supported: boolean
|
|
83
|
+
/** Store configuration; the platform SDK loads lazily on first use. */
|
|
84
|
+
configure(config: AppleAuthConfig): void
|
|
85
|
+
signIn(options?: AppleSignInOptions): Promise<SignInResult>
|
|
86
|
+
/** iOS and macOS — credential state for a prior `user.id`. */
|
|
87
|
+
getCredentialState(userId: string): Promise<AppleCredentialState>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface GoogleAuthConfig {
|
|
91
|
+
/**
|
|
92
|
+
* OAuth client id — the web client id on web (required there). On iOS it
|
|
93
|
+
* is optional when `GIDClientID` is in the app's Info.plist; on Android the
|
|
94
|
+
* id comes from the google-services resources.
|
|
95
|
+
*/
|
|
96
|
+
clientId?: string
|
|
97
|
+
/** Your backend's web client id — makes credentials carry `authorizationCode` (server auth code). */
|
|
98
|
+
serverClientId?: string
|
|
99
|
+
scopes?: string[]
|
|
100
|
+
/** Restrict to a Google Workspace domain. */
|
|
101
|
+
hostedDomain?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface GoogleSignInOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Web only — nonce bound into the id token. Scope requests go through
|
|
107
|
+
* `GoogleAuthConfig.scopes`; neither provider SDK takes per-call scopes.
|
|
108
|
+
*/
|
|
109
|
+
nonce?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface GoogleAuth {
|
|
113
|
+
/**
|
|
114
|
+
* Whether the provider SDK can run here — iOS/Android + browsers. False on
|
|
115
|
+
* macOS (use a hosted `authSession` flow instead). Android devices without
|
|
116
|
+
* Play services still report true — the sign-in call surfaces the failure.
|
|
117
|
+
*/
|
|
118
|
+
readonly supported: boolean
|
|
119
|
+
configure(config?: GoogleAuthConfig): Promise<void>
|
|
120
|
+
signIn(options?: GoogleSignInOptions): Promise<SignInResult>
|
|
121
|
+
/** Clear the SDK's account selection so the next `signIn` re-prompts. */
|
|
122
|
+
signOut(): Promise<void>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface SignInButtonBaseProps {
|
|
126
|
+
id?: string
|
|
127
|
+
className?: any
|
|
128
|
+
style?: any
|
|
129
|
+
disabled?: boolean
|
|
130
|
+
/** Tap hook — fires alongside the sign-in flow (not a gate). */
|
|
131
|
+
onPress?: () => void
|
|
132
|
+
/** Called with the outcome of the flow the button started. */
|
|
133
|
+
onResult?: (result: SignInResult) => void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface AppleSignInButtonProps extends SignInButtonBaseProps {
|
|
137
|
+
/** Label variant — 'default' is "Sign in with Apple". */
|
|
138
|
+
type?: 'default' | 'signUp' | 'continue'
|
|
139
|
+
theme?: 'black' | 'white' | 'whiteOutline'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface GoogleSignInButtonProps extends SignInButtonBaseProps {
|
|
143
|
+
/** Native button color scheme — 'dark'/'light'/'auto'. */
|
|
144
|
+
theme?: 'dark' | 'light' | 'auto'
|
|
145
|
+
/** 'wide' and 'icon' are native-only shapes; web renders 'standard'. */
|
|
146
|
+
variant?: 'standard' | 'wide' | 'icon'
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"files": [
|
|
4
|
+
"AppleSignInButton.d.ts",
|
|
5
|
+
"AppleSignInButton.web.d.ts",
|
|
6
|
+
"GoogleSignInButton.d.ts",
|
|
7
|
+
"GoogleSignInButton.web.d.ts",
|
|
8
|
+
"apple.d.ts",
|
|
9
|
+
"apple.web.d.ts",
|
|
10
|
+
"google.d.ts",
|
|
11
|
+
"google.web.d.ts",
|
|
12
|
+
"index.d.ts",
|
|
13
|
+
"index.web.d.ts",
|
|
14
|
+
"types.d.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|