@octane-xplat/auth 0.0.0-stage → 0.8.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/native/AppleSignInButton.tsrx.js +63 -0
  3. package/dist/native/GoogleSignInButton.tsrx.js +35 -0
  4. package/dist/native/apple.js +63 -0
  5. package/dist/native/google.js +50 -0
  6. package/dist/native/index.js +5 -0
  7. package/dist/native/octane-client-build.json +8 -0
  8. package/dist/web/AppleSignInButton.web.tsrx.js +71 -0
  9. package/dist/web/GoogleSignInButton.web.tsrx.js +47 -0
  10. package/dist/web/apple.web.js +86 -0
  11. package/dist/web/google.web.js +130 -0
  12. package/dist/web/index.js +5 -0
  13. package/dist/web/octane-client-build.json +8 -0
  14. package/package.json +83 -3
  15. package/src/AppleSignInButton.macos.tsrx +36 -0
  16. package/src/AppleSignInButton.tsrx +46 -0
  17. package/src/AppleSignInButton.web.tsrx +54 -0
  18. package/src/GoogleSignInButton.macos.tsrx +7 -0
  19. package/src/GoogleSignInButton.tsrx +31 -0
  20. package/src/GoogleSignInButton.web.tsrx +23 -0
  21. package/src/apple.macos.ts +277 -0
  22. package/src/apple.ts +95 -0
  23. package/src/apple.web.ts +111 -0
  24. package/src/google.macos.ts +18 -0
  25. package/src/google.ts +61 -0
  26. package/src/google.web.ts +163 -0
  27. package/src/index.macos.ts +19 -0
  28. package/src/index.ts +21 -0
  29. package/src/index.web.ts +21 -0
  30. package/src/native-elements.d.ts +10 -0
  31. package/src/types.ts +147 -0
  32. package/types/generated/.tsrx-typegen-manifest.json +16 -0
  33. package/types/generated/AppleSignInButton.d.ts +5 -0
  34. package/types/generated/AppleSignInButton.web.d.ts +6 -0
  35. package/types/generated/GoogleSignInButton.d.ts +5 -0
  36. package/types/generated/GoogleSignInButton.web.d.ts +6 -0
  37. package/types/generated/apple.d.ts +2 -0
  38. package/types/generated/apple.web.d.ts +2 -0
  39. package/types/generated/google.d.ts +2 -0
  40. package/types/generated/google.web.d.ts +8 -0
  41. package/types/generated/index.d.ts +5 -0
  42. package/types/generated/index.web.d.ts +5 -0
  43. package/types/generated/types.d.ts +133 -0
  44. package/types/index.macos.d.ts +36 -0
  45. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "@octane-xplat/auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.8.0",
4
+ "description": "Sign in with Apple + Google Sign-In for Octane xplat — native SDKs on iOS/Android, platform web SDKs in the browser",
5
+ "keywords": [
6
+ "octane",
7
+ "xplat",
8
+ "cross-platform",
9
+ "auth",
10
+ "sign-in",
11
+ "apple",
12
+ "google",
13
+ "oauth",
14
+ "nativescript",
15
+ "ios",
16
+ "android",
17
+ "web"
18
+ ],
19
+ "license": "MIT",
20
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/auth",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/octane-xplat/octane-xplat.git",
24
+ "directory": "packages/auth"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "types",
29
+ "src"
30
+ ],
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "macos": {
35
+ "types": "./types/index.macos.d.ts",
36
+ "default": "./src/index.macos.ts"
37
+ },
38
+ "web": {
39
+ "types": "./types/generated/index.web.d.ts",
40
+ "default": "./dist/web/index.js"
41
+ },
42
+ "native": {
43
+ "types": "./types/generated/index.d.ts",
44
+ "default": "./dist/native/index.js"
45
+ },
46
+ "types": "./types/generated/index.d.ts",
47
+ "default": "./dist/native/index.js"
48
+ }
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@nativescript/apple-sign-in": "2.0.0",
55
+ "@nativescript/google-signin": "2.1.1"
56
+ },
57
+ "devDependencies": {
58
+ "@nativescript-community/octane": "0.2.4",
59
+ "@nativescript/core": "9.1.2",
60
+ "@octanejs/vite-plugin": "0.1.61",
61
+ "@tsrx/typescript-plugin": "0.4.11",
62
+ "octane": "0.6.3",
63
+ "tsrx-typegen": "0.2.0",
64
+ "typescript": "5.9.3",
65
+ "vite": "8.3.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@nativescript-community/octane": ">=0.2.1 <1",
69
+ "@nativescript/core": ">=9.1.0 <10",
70
+ "octane": ">=0.6.3 <1"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@nativescript-community/octane": {
74
+ "optional": true
75
+ },
76
+ "@nativescript/core": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "scripts": {
81
+ "build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
82
+ "test:packed": "node tests/packed-consumer.mjs",
83
+ "typegen": "tsrx-typegen --target octane",
84
+ "pack:check": "tsrx-typegen --pack-check"
85
+ }
6
86
  }
@@ -0,0 +1,36 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import { appleAuth } from './apple.macos'
3
+ import type { AppleSignInButtonProps, SignInResult } from './types'
4
+
5
+ const LABELS = {
6
+ default: 'Sign in with Apple',
7
+ signUp: 'Sign up with Apple',
8
+ continue: 'Continue with Apple',
9
+ } as const
10
+
11
+ /** Sign in with Apple button — macOS leaf. The AppKit renderer has no
12
+ * ASAuthorizationAppleIDButton in its vocabulary, so this is a flexbox row
13
+ * with a click-gesture tap (the same shape as Pressable/controls.macos)
14
+ * that runs the AuthenticationServices flow and reports through `onResult`.
15
+ * onPress on the intrinsic <button> is not used — the universal compile
16
+ * emits DOM-only delegateEvents for it. */
17
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
18
+ <flexboxlayout
19
+ id={props.id}
20
+ className={['vx-button', props.className]}
21
+ style={props.style}
22
+ accessibilityLabel={LABELS[props.type ?? 'default']}
23
+ onTap={
24
+ props.disabled === true
25
+ ? undefined
26
+ : () => {
27
+ props.onPress?.()
28
+ if (props.onResult) {
29
+ void appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result))
30
+ }
31
+ }
32
+ }
33
+ >
34
+ <label text={LABELS[props.type ?? 'default']} />
35
+ </flexboxlayout>
36
+ }
@@ -0,0 +1,46 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane';
3
+ import * as applePlugin from '@nativescript/apple-sign-in';
4
+ import { appleAuth } from './apple';
5
+ import type { AppleSignInButtonProps, SignInResult } from './types';
6
+
7
+ // The plugin's SignInButton wraps ASAuthorizationAppleIDButton — iOS only
8
+ // (the class is absent from the plugin's Android module), so register it
9
+ // conditionally and fall back to a plain button everywhere else.
10
+ const PluginButton = (applePlugin as any).SignInButton;
11
+ if (PluginButton) {
12
+ registerElement('applesigninbutton', PluginButton);
13
+ }
14
+
15
+ /** Sign in with Apple button — the platform-authentic
16
+ * ASAuthorizationAppleIDButton on iOS, a plain fallback button elsewhere.
17
+ * A tap starts `appleAuth.signIn()`; `onResult` reports the outcome. */
18
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
19
+ const run = () => {
20
+ props.onPress?.();
21
+ if (props.onResult) {
22
+ appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
23
+ }
24
+ };
25
+
26
+ @if (PluginButton && appleAuth.supported) {
27
+ <applesigninbutton
28
+ id={props.id}
29
+ className={props.className}
30
+ style={props.style}
31
+ type={props.type ?? 'default'}
32
+ buttonStyle={props.theme ?? 'black'}
33
+ isEnabled={props.disabled !== true}
34
+ onTap={run}
35
+ />
36
+ } @else {
37
+ <button
38
+ id={props.id}
39
+ className={props.className}
40
+ style={props.style}
41
+ text={props.type === 'signUp' ? 'Sign up with Apple' : props.type === 'continue' ? 'Continue with Apple' : 'Sign in with Apple'}
42
+ isEnabled={props.disabled !== true}
43
+ onTap={run}
44
+ />
45
+ }
46
+ }
@@ -0,0 +1,54 @@
1
+ import { appleAuth } from './apple.web';
2
+ import type { AppleSignInButtonProps, SignInResult } from './types';
3
+
4
+ const LABELS = {
5
+ default: 'Sign in with Apple',
6
+ signUp: 'Sign up with Apple',
7
+ continue: 'Continue with Apple',
8
+ } as const;
9
+
10
+ const THEMES = {
11
+ black: { background: '#000', color: '#fff', border: '1px solid #000' },
12
+ white: { background: '#fff', color: '#000', border: '1px solid #fff' },
13
+ whiteOutline: { background: '#fff', color: '#000', border: '1px solid #000' },
14
+ } as const;
15
+
16
+ /** Sign in with Apple button — web leaf. Apple's JS API renders official
17
+ * markup only through its own stylesheet hooks, so this is a styled fallback
18
+ * matching Apple's published button spec; the click runs the popup flow via
19
+ * `appleAuth.signIn()` and reports through `onResult`. */
20
+ export function AppleSignInButton(props: AppleSignInButtonProps) @{
21
+ const theme = THEMES[props.theme ?? 'black'];
22
+
23
+ <button
24
+ type="button"
25
+ id={props.id}
26
+ className={props.className}
27
+ disabled={props.disabled}
28
+ style={{
29
+ display: 'inline-flex',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ gap: 6,
33
+ minHeight: 40,
34
+ padding: '0 16px',
35
+ borderRadius: 6,
36
+ fontFamily: "-apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif",
37
+ fontSize: 15,
38
+ cursor: props.disabled ? 'default' : 'pointer',
39
+ ...theme,
40
+ ...props.style,
41
+ }}
42
+ onClick={() => {
43
+ props.onPress?.();
44
+ if (props.onResult) {
45
+ void appleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
46
+ }
47
+ }}
48
+ >
49
+ <svg width="15" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
50
+ <path d="M15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09z" />
51
+ </svg>
52
+ {LABELS[props.type ?? 'default']}
53
+ </button>
54
+ }
@@ -0,0 +1,7 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import type { GoogleSignInButtonProps } from './types'
3
+
4
+ /** Explicit placeholder — the AppKit host has no Google sign-in button. */
5
+ export function GoogleSignInButton(_props: GoogleSignInButtonProps) @{
6
+ <label className="vx-unsupported">Google Sign-In is unsupported by the AppKit host</label>
7
+ }
@@ -0,0 +1,31 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane';
3
+ import { GoogleSignInButton as NSGoogleSignInButton } from '@nativescript/google-signin';
4
+ import { googleAuth } from './google';
5
+ import type { GoogleSignInButtonProps, SignInResult } from './types';
6
+
7
+ // GIDSignInButton (iOS) / the plugin's Google button (Android). The plugin
8
+ // emits the standard `tap` event — the driver's onTap alias wires it.
9
+ registerElement('googlesigninbutton', NSGoogleSignInButton);
10
+
11
+ /** Google Sign-In button — the provider's button view on iOS/Android. A tap
12
+ * starts `googleAuth.signIn()`; `onResult` reports the outcome. Requires a
13
+ * `googleAuth.configure()` call first where the platform needs it. */
14
+ export function GoogleSignInButton(props: GoogleSignInButtonProps) @{
15
+ const run = () => {
16
+ props.onPress?.();
17
+ if (props.onResult) {
18
+ googleAuth.signIn().then((result: SignInResult) => props.onResult?.(result));
19
+ }
20
+ };
21
+
22
+ <googlesigninbutton
23
+ id={props.id}
24
+ className={props.className}
25
+ style={props.style}
26
+ colorScheme={props.theme ?? 'auto'}
27
+ colorStyle={props.variant ?? 'standard'}
28
+ isEnabled={props.disabled !== true}
29
+ onTap={run}
30
+ />
31
+ }
@@ -0,0 +1,23 @@
1
+ import { renderGoogleButton } from './google.web';
2
+ import type { GoogleSignInButtonProps, SignInResult } from './types';
3
+
4
+ /** Google Sign-In button — web leaf. Renders the official GIS button via
5
+ * `renderButton` when a `clientId` is configured; without one it falls back
6
+ * to a plain button that runs `googleAuth.signIn()` (One Tap). `onResult`
7
+ * reports the outcome either way. */
8
+ export function GoogleSignInButton(props: GoogleSignInButtonProps) @{
9
+ <div
10
+ id={props.id}
11
+ className={props.className}
12
+ style={props.style}
13
+ ref={(el: HTMLElement | null) => {
14
+ if (el && !props.disabled) {
15
+ void renderGoogleButton(
16
+ el,
17
+ { theme: props.theme, variant: props.variant },
18
+ (result: SignInResult) => props.onResult?.(result),
19
+ );
20
+ }
21
+ }}
22
+ />
23
+ }
@@ -0,0 +1,277 @@
1
+ // Sign in with Apple — macOS leaf over AuthenticationServices via the
2
+ // appkit-node-api ObjC bridge (the same framework the iOS plugin wraps,
3
+ // available since macOS 10.15). The `com.apple.developer.applesignin`
4
+ // entitlement must sit on the packaged app for the sheet to present;
5
+ // hosts without it still load the framework and surface the OS error.
6
+ import type {
7
+ AppleAuth,
8
+ AppleAuthConfig,
9
+ AppleCredentialState,
10
+ AppleSignInOptions,
11
+ AuthCredential,
12
+ SignInResult,
13
+ } from './types'
14
+
15
+ declare const ASAuthorizationAppleIDProvider: any
16
+ declare const ASAuthorizationController: any
17
+ declare const ASAuthorizationControllerDelegate: any
18
+ declare const ASAuthorizationControllerPresentationContextProviding: any
19
+ declare const ASAuthorizationScopeEmail: any
20
+ declare const ASAuthorizationScopeFullName: any
21
+ declare const NSApplication: any
22
+ declare const NSString: any
23
+ declare const NSObject: any
24
+ declare const interop: any
25
+ declare const objc: any
26
+
27
+ // Import before the NSObject.extend classes below are built — their
28
+ // protocol/exposed-method constants resolve at extend() time, so the
29
+ // framework's metadata has to be loaded first.
30
+ try {
31
+ objc.import('AuthenticationServices')
32
+ } catch {
33
+ // Symbols may already be bound — failures surface via `supported: false`.
34
+ }
35
+
36
+ const SCOPE_MAP: Record<string, () => any> = {
37
+ email: () => ASAuthorizationScopeEmail,
38
+ name: () => ASAuthorizationScopeFullName,
39
+ }
40
+
41
+ function isCancel(error: unknown): boolean {
42
+ // ASAuthorizationError.canceled = 1001 (NSError.code).
43
+ return (error as any)?.code === 1001 || /cancel/i.test(String((error as any)?.localizedDescription ?? error))
44
+ }
45
+
46
+ function utf8Bytes(input: string): Uint8Array {
47
+ const data = NSString.stringWithString(input).dataUsingEncoding(4 /* NSUTF8StringEncoding */)
48
+ return new Uint8Array(interop.bufferFromData(data))
49
+ }
50
+
51
+ // FIPS 180-4 — the request nonce must be the hex SHA-256 of the app's nonce
52
+ // (same transform the iOS plugin applies via CommonCrypto, which has no
53
+ // node-api metadata). Kept local to stay dependency-free.
54
+ function sha256Hex(input: string): string {
55
+ const K = new Uint32Array([
56
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
57
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
58
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
59
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
60
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
61
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
62
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
63
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
64
+ ])
65
+
66
+ const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19])
67
+ const bytes = utf8Bytes(input)
68
+ const bitLen = bytes.length * 8
69
+ const padded = new Uint8Array((bytes.length + 9 + 63) & ~63)
70
+ padded.set(bytes)
71
+ padded[bytes.length] = 0x80
72
+ const dv = new DataView(padded.buffer)
73
+ dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000))
74
+ dv.setUint32(padded.length - 4, bitLen >>> 0)
75
+ const w = new Uint32Array(64)
76
+ for (let block = 0; block < padded.length; block += 64) {
77
+ for (let t = 0; t < 16; t++) {w[t] = dv.getUint32(block + t * 4)}
78
+ for (let t = 16; t < 64; t++) {
79
+ const s0 = (w[t - 15] >>> 7 | w[t - 15] << 25) ^ (w[t - 15] >>> 18 | w[t - 15] << 14) ^ (w[t - 15] >>> 3)
80
+ const s1 = (w[t - 2] >>> 17 | w[t - 2] << 15) ^ (w[t - 2] >>> 19 | w[t - 2] << 13) ^ (w[t - 2] >>> 10)
81
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
82
+ }
83
+
84
+ let [a, b, c, d, e, f, g, h] = H
85
+ for (let t = 0; t < 64; t++) {
86
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)
87
+ const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[t] + w[t]) >>> 0
88
+ const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)
89
+ const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0
90
+ h = g; g = f; f = e; e = (d + t1) >>> 0
91
+ d = c; c = b; b = a; a = (t1 + t2) >>> 0
92
+ }
93
+
94
+ H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0
95
+ H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0
96
+ }
97
+
98
+ return [...H].map((x) => x.toString(16).padStart(8, '0')).join('')
99
+ }
100
+
101
+ function toCredential(credential: any): AuthCredential {
102
+ const tokenData = credential.valueForKey('identityToken')
103
+ const codeData = credential.valueForKey('authorizationCode')
104
+ const fullName = credential.fullName
105
+ const name =
106
+ [fullName?.givenName, fullName?.familyName].filter(Boolean).join(' ') || undefined
107
+
108
+ return {
109
+ provider: 'apple',
110
+ idToken: tokenData ? String(NSString.alloc().initWithDataEncoding(tokenData, 4)) : undefined,
111
+ authorizationCode: codeData ? String(NSString.alloc().initWithDataEncoding(codeData, 4)) : undefined,
112
+ scopes: (credential.authorizedScopes ? Array.from(credential.authorizedScopes as any) : []).map((s: any) =>
113
+ s === ASAuthorizationScopeEmail ? 'email' : s === ASAuthorizationScopeFullName ? 'name' : String(s),
114
+ ),
115
+ user: {
116
+ id: String(credential.user ?? ''),
117
+ email: credential.email ? String(credential.email) : undefined,
118
+ name,
119
+ givenName: fullName?.givenName ? String(fullName.givenName) : undefined,
120
+ familyName: fullName?.familyName ? String(fullName.familyName) : undefined,
121
+ },
122
+ }
123
+ }
124
+
125
+ let frameworkReady: boolean | null = null
126
+
127
+ function frameworkAvailable(): boolean {
128
+ if (frameworkReady !== null) {
129
+ return frameworkReady
130
+ }
131
+
132
+ try {
133
+ objc.import('AuthenticationServices')
134
+ } catch {
135
+ // The framework ships in every macOS ≥ 10.15 SDK — a failed import just
136
+ // means the symbols still resolve lazily below.
137
+ }
138
+
139
+ frameworkReady = typeof ASAuthorizationAppleIDProvider !== 'undefined'
140
+ return frameworkReady
141
+ }
142
+
143
+ // NSObject.extend is the registration shape this node-api runtime binds
144
+ // (class syntax + NativeClass registers the class but not the methods —
145
+ // verified by probe: respondsToSelector is only true for .extend). Classes
146
+ // are built lazily so the protocol constants resolve after import.
147
+ let AppleAuthDelegate: any = null
148
+ let AppleAuthAnchor: any = null
149
+
150
+ function ensureClasses() {
151
+ if (AppleAuthDelegate !== null) {
152
+ return
153
+ }
154
+
155
+ AppleAuthDelegate = NSObject.extend(
156
+ {
157
+ authorizationControllerDidCompleteWithAuthorization(_controller: any, authorization: any) {
158
+ const finish = activeFinish
159
+ activeFinish = null
160
+ finish?.({ status: 'success', credential: toCredential(authorization.credential) })
161
+ },
162
+ authorizationControllerDidCompleteWithError(_controller: any, error: any) {
163
+ const finish = activeFinish
164
+ activeFinish = null
165
+ finish?.(
166
+ isCancel(error)
167
+ ? { status: 'cancelled' }
168
+ : { status: 'error', message: String((error as any)?.localizedDescription ?? error) },
169
+ )
170
+ },
171
+ },
172
+ {
173
+ protocols: [ASAuthorizationControllerDelegate],
174
+ exposedMethods: {
175
+ 'authorizationController:didCompleteWithAuthorization:': {
176
+ params: [interop.types.id, interop.types.id],
177
+ returns: interop.types.void,
178
+ },
179
+ 'authorizationController:didCompleteWithError:': {
180
+ params: [interop.types.id, interop.types.id],
181
+ returns: interop.types.void,
182
+ },
183
+ },
184
+ },
185
+ )
186
+
187
+ AppleAuthAnchor = NSObject.extend(
188
+ {
189
+ presentationAnchorForAuthorizationController(_controller: any): any {
190
+ const app = NSApplication.sharedApplication
191
+ return app.keyWindow ?? app.mainWindow ?? app.windows?.firstObject
192
+ },
193
+ },
194
+ {
195
+ protocols: [ASAuthorizationControllerPresentationContextProviding],
196
+ exposedMethods: {
197
+ 'presentationAnchorForAuthorizationController:': {
198
+ params: [interop.types.id],
199
+ returns: interop.types.id,
200
+ },
201
+ },
202
+ },
203
+ )
204
+ }
205
+
206
+ // Retain the ObjC objects + resolver for the ceremony's lifetime — the
207
+ // bridge sees no JS-side reference after signIn returns.
208
+ let activeController: any = null
209
+ let activeDelegate: any = null
210
+ let activeAnchor: any = null
211
+ let activeFinish: ((result: SignInResult) => void) | null = null
212
+
213
+ export const appleAuth: AppleAuth = {
214
+ get supported() {
215
+ return frameworkAvailable()
216
+ },
217
+ configure(_config: AppleAuthConfig) {
218
+ // No native configuration — the flow is bound to the app id entitlement.
219
+ },
220
+ async signIn(options?: AppleSignInOptions): Promise<SignInResult> {
221
+ if (!frameworkAvailable()) {
222
+ return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
223
+ }
224
+
225
+ if (activeController) {
226
+ return { status: 'error', message: 'a Sign in with Apple flow is already active' }
227
+ }
228
+
229
+ return new Promise<SignInResult>((resolve) => {
230
+ const finish = (result: SignInResult) => {
231
+ activeController = null
232
+ activeDelegate = null
233
+ activeAnchor = null
234
+ activeFinish = null
235
+ resolve(result)
236
+ }
237
+
238
+ ensureClasses()
239
+ activeDelegate = AppleAuthDelegate.new()
240
+ activeFinish = finish
241
+ const provider = ASAuthorizationAppleIDProvider.new()
242
+ const request = provider.createRequest()
243
+ if (options?.scopes?.length) {
244
+ request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
245
+ }
246
+
247
+ if (options?.nonce !== undefined) {
248
+ request.nonce = sha256Hex(options.nonce)
249
+ }
250
+
251
+ activeAnchor = AppleAuthAnchor.new()
252
+ activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([request])
253
+ activeController.delegate = activeDelegate
254
+ activeController.presentationContextProvider = activeAnchor
255
+ activeController.performRequests()
256
+ })
257
+ },
258
+ async getCredentialState(userId: string): Promise<AppleCredentialState> {
259
+ if (!frameworkAvailable()) {
260
+ return 'unknown'
261
+ }
262
+
263
+ return new Promise<AppleCredentialState>((resolve) => {
264
+ ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
265
+ userId,
266
+ (state: number, error: any) => {
267
+ if (error) {
268
+ resolve('unknown')
269
+ return
270
+ }
271
+
272
+ resolve(state === 1 ? 'authorized' : state === 0 ? 'revoked' : state === 3 ? 'transferred' : 'notFound')
273
+ },
274
+ )
275
+ })
276
+ },
277
+ }
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
+ }