@authara/react 0.0.0-stage → 0.0.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 +128 -2
- package/dist/AppleButton.d.ts +17 -0
- package/dist/AppleButton.js +71 -0
- package/dist/GoogleButton.d.ts +15 -0
- package/dist/GoogleButton.js +75 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.js +22 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/provider-sdks.d.ts +61 -0
- package/dist/provider-sdks.js +72 -0
- package/dist/services.d.ts +34 -0
- package/dist/services.js +120 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Lupatsiy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @authara/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official React components for Authara's Google and Apple browser flows.
|
|
4
|
+
|
|
5
|
+
The package owns provider SDK loading and React lifecycle integration. HTTP,
|
|
6
|
+
cookies, CSRF protection, sessions, and generated Authara API operations remain
|
|
7
|
+
in [`@authara/browser`](https://www.npmjs.com/package/@authara/browser).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @authara/react @authara/browser react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Create one browser client and place one provider near the root of the page:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { AutharaBrowserClient } from "@authara/browser";
|
|
21
|
+
import {
|
|
22
|
+
AppleButton,
|
|
23
|
+
AutharaProvider,
|
|
24
|
+
GoogleButton
|
|
25
|
+
} from "@authara/react";
|
|
26
|
+
|
|
27
|
+
const authara = new AutharaBrowserClient();
|
|
28
|
+
|
|
29
|
+
export function Login() {
|
|
30
|
+
return (
|
|
31
|
+
<AutharaProvider client={authara}>
|
|
32
|
+
<GoogleButton
|
|
33
|
+
onCredential={async ({ credential, nonce }) => {
|
|
34
|
+
await authara.loginWithGoogle({
|
|
35
|
+
audience: "app",
|
|
36
|
+
body: { credential, nonce }
|
|
37
|
+
});
|
|
38
|
+
}}
|
|
39
|
+
onError={console.error}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<AppleButton
|
|
43
|
+
onAuthorization={async ({ code, state }) => {
|
|
44
|
+
await authara.loginWithApple({
|
|
45
|
+
audience: "app",
|
|
46
|
+
body: { code, state }
|
|
47
|
+
});
|
|
48
|
+
}}
|
|
49
|
+
onError={console.error}
|
|
50
|
+
/>
|
|
51
|
+
</AutharaProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The buttons return provider proof instead of choosing an Authara operation.
|
|
57
|
+
The same components therefore work for login, provider linking,
|
|
58
|
+
reauthentication, invitations, and account-recovery proof.
|
|
59
|
+
|
|
60
|
+
`AutharaProvider` is deliberately small. It contains no authentication or
|
|
61
|
+
session state. It coordinates the page-global third-party SDK instances so
|
|
62
|
+
Google is initialized once and callbacks are routed to the correct button.
|
|
63
|
+
|
|
64
|
+
## Provider availability
|
|
65
|
+
|
|
66
|
+
Authara returns `404 Not Found` from an options operation when that provider is
|
|
67
|
+
disabled. The corresponding button then renders `fallback` (which defaults to
|
|
68
|
+
`null`) and calls `onUnavailable`.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<GoogleButton
|
|
72
|
+
fallback={<p>Google sign-in is not configured.</p>}
|
|
73
|
+
onUnavailable={() => reportProviderUnavailable("google")}
|
|
74
|
+
onCredential={handleGoogleCredential}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Apple user cancellation calls `onCancel`; it is not reported through
|
|
79
|
+
`onError`.
|
|
80
|
+
|
|
81
|
+
## Appearance
|
|
82
|
+
|
|
83
|
+
`GoogleButton` exposes the official Google button configuration: `type`,
|
|
84
|
+
`theme`, `size`, `text`, `shape`, `logo_alignment`, `width`, and `locale`.
|
|
85
|
+
|
|
86
|
+
`AppleButton` exposes `color`, `border`, `type`, `mode`, and `locale`. Width and
|
|
87
|
+
height are set through the normal React `style` prop. Provider branding remains
|
|
88
|
+
rendered by the official provider SDKs.
|
|
89
|
+
|
|
90
|
+
## Contract synchronization
|
|
91
|
+
|
|
92
|
+
The components never contain Authara route strings. They consume the generated
|
|
93
|
+
`AutharaBrowserClient` operation methods:
|
|
94
|
+
|
|
95
|
+
- `getGoogleLoginOptions()`
|
|
96
|
+
- `getAppleLoginOptions()`
|
|
97
|
+
|
|
98
|
+
`.contract/manifest.json` records the exact browser SDK release and its
|
|
99
|
+
immutable Authara Core OpenAPI provenance. `npm run check:contract` performs a
|
|
100
|
+
TypeScript structural compatibility check against that exact browser SDK.
|
|
101
|
+
|
|
102
|
+
`.github/workflows/sync-browser.yaml` receives each
|
|
103
|
+
`authara-browser-released` event, installs the exact released browser package,
|
|
104
|
+
copies its Core provenance, runs the full checks, and opens an automated pull
|
|
105
|
+
request. This keeps route and schema changes flowing in this order:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
Authara Core OpenAPI release
|
|
109
|
+
-> generated @authara/browser release
|
|
110
|
+
-> @authara/react contract check and dependency PR
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The initial manifest is intentionally marked `unreleased`. Publishing remains
|
|
114
|
+
blocked until a browser SDK release containing the Apple operations has passed
|
|
115
|
+
the synchronization workflow.
|
|
116
|
+
|
|
117
|
+
## Design guarantees
|
|
118
|
+
|
|
119
|
+
- No hidden login or redirect behavior
|
|
120
|
+
- No Authara route duplication
|
|
121
|
+
- Provider scripts are loaded once
|
|
122
|
+
- Safe effect cleanup for React Strict Mode
|
|
123
|
+
- Fresh Apple state and nonce immediately before authorization
|
|
124
|
+
- Disabled providers disappear cleanly
|
|
125
|
+
- React 18 and 19 peer support
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
2
|
+
import type { MaybePromise } from "./contract.js";
|
|
3
|
+
import type { AppleAuthorization } from "./services.js";
|
|
4
|
+
export interface AppleButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError"> {
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
locale?: string;
|
|
7
|
+
color?: "black" | "white";
|
|
8
|
+
border?: boolean;
|
|
9
|
+
type?: "sign-in" | "continue" | "sign-up";
|
|
10
|
+
mode?: "center-align" | "left-align" | "logo-only";
|
|
11
|
+
fallback?: ReactNode;
|
|
12
|
+
onAuthorization: (authorization: AppleAuthorization) => MaybePromise<void>;
|
|
13
|
+
onCancel?: () => void;
|
|
14
|
+
onError?: (error: Error) => void;
|
|
15
|
+
onUnavailable?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function AppleButton({ disabled, locale, color, border, type, mode, fallback, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }: AppleButtonProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, } from "react";
|
|
3
|
+
import { isAppleCancellation, isProviderUnavailable, toError, } from "./errors.js";
|
|
4
|
+
import { useAutharaServices } from "./context.js";
|
|
5
|
+
export function AppleButton({ disabled = false, locale = "en_US", color = "black", border = true, type = "continue", mode = "center-align", fallback = null, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }) {
|
|
6
|
+
const { apple } = useAutharaServices();
|
|
7
|
+
const targetRef = useRef(null);
|
|
8
|
+
const authorizationRef = useRef(onAuthorization);
|
|
9
|
+
const cancelRef = useRef(onCancel);
|
|
10
|
+
const errorRef = useRef(onError);
|
|
11
|
+
const unavailableRef = useRef(onUnavailable);
|
|
12
|
+
const [available, setAvailable] = useState(true);
|
|
13
|
+
const [ready, setReady] = useState(false);
|
|
14
|
+
const [busy, setBusy] = useState(false);
|
|
15
|
+
authorizationRef.current = onAuthorization;
|
|
16
|
+
cancelRef.current = onCancel;
|
|
17
|
+
errorRef.current = onError;
|
|
18
|
+
unavailableRef.current = onUnavailable;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const element = targetRef.current;
|
|
21
|
+
if (!element)
|
|
22
|
+
return;
|
|
23
|
+
let active = true;
|
|
24
|
+
void apple
|
|
25
|
+
.render(element, locale)
|
|
26
|
+
.then(() => {
|
|
27
|
+
if (active)
|
|
28
|
+
setReady(true);
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
if (!active)
|
|
32
|
+
return;
|
|
33
|
+
if (isProviderUnavailable(error)) {
|
|
34
|
+
setAvailable(false);
|
|
35
|
+
unavailableRef.current?.();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
errorRef.current?.(toError(error, "Apple authentication is unavailable."));
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
active = false;
|
|
42
|
+
};
|
|
43
|
+
}, [apple, border, color, locale, mode, type]);
|
|
44
|
+
const beginAuthorization = (event) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
if (!ready || disabled || busy)
|
|
48
|
+
return;
|
|
49
|
+
setBusy(true);
|
|
50
|
+
void apple
|
|
51
|
+
.authorize(locale)
|
|
52
|
+
.then((authorization) => authorizationRef.current(authorization))
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
if (isAppleCancellation(error)) {
|
|
55
|
+
cancelRef.current?.();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
errorRef.current?.(toError(error, "Apple authentication failed."));
|
|
59
|
+
})
|
|
60
|
+
.finally(() => setBusy(false));
|
|
61
|
+
};
|
|
62
|
+
if (!available)
|
|
63
|
+
return _jsx(_Fragment, { children: fallback });
|
|
64
|
+
const inactive = disabled || busy;
|
|
65
|
+
return (_jsx("div", { ...divProps, "aria-busy": busy || undefined, "aria-disabled": inactive || undefined, "data-authara-provider": "apple", "data-authara-ready": ready ? "true" : "false", onClickCapture: beginAuthorization, style: {
|
|
66
|
+
width: "100%",
|
|
67
|
+
height: 44,
|
|
68
|
+
...style,
|
|
69
|
+
pointerEvents: inactive ? "none" : style?.pointerEvents,
|
|
70
|
+
}, children: _jsx("div", { ref: targetRef, "data-color": color, "data-border": String(border), "data-type": type, "data-mode": mode, style: { width: "100%", height: "100%" } }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
2
|
+
import type { MaybePromise } from "./contract.js";
|
|
3
|
+
import type { GoogleButtonConfiguration } from "./provider-sdks.js";
|
|
4
|
+
import type { GoogleCredential } from "./services.js";
|
|
5
|
+
type GoogleAppearance = Pick<GoogleButtonConfiguration, "type" | "theme" | "size" | "text" | "shape" | "logo_alignment" | "width">;
|
|
6
|
+
export interface GoogleButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError">, GoogleAppearance {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
locale?: string;
|
|
9
|
+
fallback?: ReactNode;
|
|
10
|
+
onCredential: (credential: GoogleCredential) => MaybePromise<void>;
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
onUnavailable?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function GoogleButton({ disabled, locale, fallback, onCredential, onError, onUnavailable, type, theme, size, text, shape, logo_alignment, width, style, ...divProps }: GoogleButtonProps): React.JSX.Element | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, } from "react";
|
|
3
|
+
import { isProviderUnavailable, toError } from "./errors.js";
|
|
4
|
+
import { useAutharaServices } from "./context.js";
|
|
5
|
+
export function GoogleButton({ disabled = false, locale, fallback = null, onCredential, onError, onUnavailable, type = "standard", theme = "outline", size = "large", text = "continue_with", shape = "rectangular", logo_alignment = "left", width, style, ...divProps }) {
|
|
6
|
+
const { google } = useAutharaServices();
|
|
7
|
+
const targetRef = useRef(null);
|
|
8
|
+
const credentialRef = useRef(onCredential);
|
|
9
|
+
const errorRef = useRef(onError);
|
|
10
|
+
const unavailableRef = useRef(onUnavailable);
|
|
11
|
+
const disabledRef = useRef(disabled);
|
|
12
|
+
const [available, setAvailable] = useState(true);
|
|
13
|
+
const [ready, setReady] = useState(false);
|
|
14
|
+
const [busy, setBusy] = useState(false);
|
|
15
|
+
credentialRef.current = onCredential;
|
|
16
|
+
errorRef.current = onError;
|
|
17
|
+
unavailableRef.current = onUnavailable;
|
|
18
|
+
disabledRef.current = disabled;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const element = targetRef.current;
|
|
21
|
+
if (!element)
|
|
22
|
+
return;
|
|
23
|
+
let active = true;
|
|
24
|
+
let dispose;
|
|
25
|
+
void google
|
|
26
|
+
.render(element, {
|
|
27
|
+
type,
|
|
28
|
+
theme,
|
|
29
|
+
size,
|
|
30
|
+
text,
|
|
31
|
+
shape,
|
|
32
|
+
logo_alignment,
|
|
33
|
+
width: width ?? Math.min(element.clientWidth || 320, 400),
|
|
34
|
+
}, locale, (credential) => {
|
|
35
|
+
if (!active || disabledRef.current)
|
|
36
|
+
return;
|
|
37
|
+
setBusy(true);
|
|
38
|
+
void Promise.resolve(credentialRef.current(credential))
|
|
39
|
+
.catch((error) => errorRef.current?.(toError(error, "Google authentication failed.")))
|
|
40
|
+
.finally(() => {
|
|
41
|
+
if (active)
|
|
42
|
+
setBusy(false);
|
|
43
|
+
});
|
|
44
|
+
})
|
|
45
|
+
.then((cleanup) => {
|
|
46
|
+
if (!active)
|
|
47
|
+
cleanup();
|
|
48
|
+
else {
|
|
49
|
+
dispose = cleanup;
|
|
50
|
+
setReady(true);
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
if (!active)
|
|
55
|
+
return;
|
|
56
|
+
if (isProviderUnavailable(error)) {
|
|
57
|
+
setAvailable(false);
|
|
58
|
+
unavailableRef.current?.();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
errorRef.current?.(toError(error, "Google authentication is unavailable."));
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
active = false;
|
|
65
|
+
dispose?.();
|
|
66
|
+
};
|
|
67
|
+
}, [google, locale, logo_alignment, shape, size, text, theme, type, width]);
|
|
68
|
+
if (!available)
|
|
69
|
+
return _jsx(_Fragment, { children: fallback });
|
|
70
|
+
const inactive = disabled || busy;
|
|
71
|
+
return (_jsx("div", { ...divProps, ref: targetRef, "aria-busy": busy || undefined, "aria-disabled": inactive || undefined, "data-authara-provider": "google", "data-authara-ready": ready ? "true" : "false", style: {
|
|
72
|
+
...style,
|
|
73
|
+
pointerEvents: inactive ? "none" : style?.pointerEvents,
|
|
74
|
+
} }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PropsWithChildren } from "react";
|
|
2
|
+
import type { AutharaReactClient } from "./contract.js";
|
|
3
|
+
import { AppleService, GoogleService } from "./services.js";
|
|
4
|
+
type AutharaReactContextValue = {
|
|
5
|
+
apple: AppleService;
|
|
6
|
+
google: GoogleService;
|
|
7
|
+
};
|
|
8
|
+
export type AutharaProviderProps = PropsWithChildren<{
|
|
9
|
+
client: AutharaReactClient;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Owns the page-wide Google and Apple SDK configuration. Use one provider per
|
|
13
|
+
* document so Google Identity Services is initialized only once.
|
|
14
|
+
*/
|
|
15
|
+
export declare function AutharaProvider({ client, children, }: AutharaProviderProps): React.JSX.Element;
|
|
16
|
+
export declare function useAutharaServices(): AutharaReactContextValue;
|
|
17
|
+
export {};
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo, } from "react";
|
|
3
|
+
import { AppleService, GoogleService } from "./services.js";
|
|
4
|
+
const AutharaContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Owns the page-wide Google and Apple SDK configuration. Use one provider per
|
|
7
|
+
* document so Google Identity Services is initialized only once.
|
|
8
|
+
*/
|
|
9
|
+
export function AutharaProvider({ client, children, }) {
|
|
10
|
+
const value = useMemo(() => ({
|
|
11
|
+
apple: new AppleService(client),
|
|
12
|
+
google: new GoogleService(client),
|
|
13
|
+
}), [client]);
|
|
14
|
+
return (_jsx(AutharaContext.Provider, { value: value, children: children }));
|
|
15
|
+
}
|
|
16
|
+
export function useAutharaServices() {
|
|
17
|
+
const value = useContext(AutharaContext);
|
|
18
|
+
if (!value) {
|
|
19
|
+
throw new Error("Authara buttons must be rendered inside AutharaProvider.");
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface GoogleLoginOptions {
|
|
2
|
+
client_id: string;
|
|
3
|
+
nonce: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AppleLoginOptions {
|
|
6
|
+
client_id: string;
|
|
7
|
+
redirect_uri: string;
|
|
8
|
+
state: string;
|
|
9
|
+
nonce: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The generated @authara/browser contract slice used by the React components.
|
|
13
|
+
* AutharaBrowserClient satisfies this interface once both providers are
|
|
14
|
+
* available in the released Core contract.
|
|
15
|
+
*/
|
|
16
|
+
export interface AutharaReactClient {
|
|
17
|
+
getGoogleLoginOptions(): Promise<GoogleLoginOptions>;
|
|
18
|
+
getAppleLoginOptions(): Promise<AppleLoginOptions>;
|
|
19
|
+
}
|
|
20
|
+
export type MaybePromise<T> = T | Promise<T>;
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function isProviderUnavailable(error) {
|
|
2
|
+
return (typeof error === "object" &&
|
|
3
|
+
error !== null &&
|
|
4
|
+
"status" in error &&
|
|
5
|
+
error.status === 404);
|
|
6
|
+
}
|
|
7
|
+
export function toError(error, fallback) {
|
|
8
|
+
if (error instanceof Error)
|
|
9
|
+
return error;
|
|
10
|
+
if (typeof error === "object" &&
|
|
11
|
+
error !== null &&
|
|
12
|
+
"message" in error &&
|
|
13
|
+
typeof error.message === "string" &&
|
|
14
|
+
error.message.trim()) {
|
|
15
|
+
return new Error(error.message.trim());
|
|
16
|
+
}
|
|
17
|
+
return new Error(fallback);
|
|
18
|
+
}
|
|
19
|
+
export function isAppleCancellation(error) {
|
|
20
|
+
if (typeof error !== "object" || error === null || !("error" in error)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return (error.error === "user_cancelled_authorize" ||
|
|
24
|
+
error.error === "popup_closed_by_user");
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AppleButton, type AppleButtonProps } from "./AppleButton.js";
|
|
2
|
+
export { GoogleButton, type GoogleButtonProps } from "./GoogleButton.js";
|
|
3
|
+
export { AutharaProvider, type AutharaProviderProps } from "./context.js";
|
|
4
|
+
export type { AppleLoginOptions, AutharaReactClient, GoogleLoginOptions, MaybePromise, } from "./contract.js";
|
|
5
|
+
export { isAppleCancellation, isProviderUnavailable } from "./errors.js";
|
|
6
|
+
export type { AppleAuthorization, GoogleCredential } from "./services.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type GoogleCredentialResponse = {
|
|
2
|
+
credential?: string;
|
|
3
|
+
select_by?: string;
|
|
4
|
+
state?: string;
|
|
5
|
+
};
|
|
6
|
+
export type GoogleButtonConfiguration = {
|
|
7
|
+
type?: "standard" | "icon";
|
|
8
|
+
theme?: "outline" | "filled_blue" | "filled_black";
|
|
9
|
+
size?: "large" | "medium" | "small";
|
|
10
|
+
text?: "signin_with" | "signup_with" | "continue_with" | "signin";
|
|
11
|
+
shape?: "rectangular" | "pill" | "circle" | "square";
|
|
12
|
+
logo_alignment?: "left" | "center";
|
|
13
|
+
width?: number;
|
|
14
|
+
locale?: string;
|
|
15
|
+
state?: string;
|
|
16
|
+
click_listener?: () => void;
|
|
17
|
+
};
|
|
18
|
+
export type GoogleIdentity = {
|
|
19
|
+
accounts: {
|
|
20
|
+
id: {
|
|
21
|
+
initialize(options: {
|
|
22
|
+
client_id: string;
|
|
23
|
+
nonce: string;
|
|
24
|
+
callback: (response: GoogleCredentialResponse) => void;
|
|
25
|
+
}): void;
|
|
26
|
+
renderButton(parent: HTMLElement, options: GoogleButtonConfiguration): void;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type AppleConfig = {
|
|
31
|
+
clientId: string;
|
|
32
|
+
scope: "email";
|
|
33
|
+
redirectURI: string;
|
|
34
|
+
state: string;
|
|
35
|
+
nonce: string;
|
|
36
|
+
usePopup: true;
|
|
37
|
+
};
|
|
38
|
+
type AppleSignInResponse = {
|
|
39
|
+
authorization?: {
|
|
40
|
+
code?: string;
|
|
41
|
+
state?: string;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export type AppleAuth = {
|
|
45
|
+
init(options: AppleConfig): void;
|
|
46
|
+
renderButton(): void;
|
|
47
|
+
signIn(options?: AppleConfig): Promise<AppleSignInResponse>;
|
|
48
|
+
};
|
|
49
|
+
declare global {
|
|
50
|
+
interface Window {
|
|
51
|
+
google?: GoogleIdentity;
|
|
52
|
+
AppleID?: {
|
|
53
|
+
auth: AppleAuth;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export declare function loadGoogleIdentity(locale?: string): Promise<GoogleIdentity>;
|
|
58
|
+
export declare function loadAppleIdentity(locale?: string): Promise<AppleAuth>;
|
|
59
|
+
/** @internal Test isolation for module-level third-party script promises. */
|
|
60
|
+
export declare function resetProviderSDKsForTests(): void;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const googleIdentityURL = "https://accounts.google.com/gsi/client";
|
|
2
|
+
const appleIdentityBaseURL = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1";
|
|
3
|
+
let googlePromise;
|
|
4
|
+
let googleLocale;
|
|
5
|
+
let applePromise;
|
|
6
|
+
let appleLocale;
|
|
7
|
+
function appendScript(url, resolveGlobal, failureMessage) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const existing = document.querySelector(`script[src="${url}"]`);
|
|
10
|
+
const script = existing ?? document.createElement("script");
|
|
11
|
+
const loaded = () => {
|
|
12
|
+
const value = resolveGlobal();
|
|
13
|
+
if (value)
|
|
14
|
+
resolve(value);
|
|
15
|
+
else
|
|
16
|
+
reject(new Error(failureMessage));
|
|
17
|
+
};
|
|
18
|
+
const failed = () => reject(new Error(failureMessage));
|
|
19
|
+
script.addEventListener("load", loaded, { once: true });
|
|
20
|
+
script.addEventListener("error", failed, { once: true });
|
|
21
|
+
if (!existing) {
|
|
22
|
+
script.src = url;
|
|
23
|
+
script.async = true;
|
|
24
|
+
document.head.appendChild(script);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function loadGoogleIdentity(locale) {
|
|
29
|
+
if (window.google?.accounts?.id)
|
|
30
|
+
return Promise.resolve(window.google);
|
|
31
|
+
if (googlePromise) {
|
|
32
|
+
if (locale !== googleLocale) {
|
|
33
|
+
return Promise.reject(new Error("Google Identity has already been loaded with another locale."));
|
|
34
|
+
}
|
|
35
|
+
return googlePromise;
|
|
36
|
+
}
|
|
37
|
+
googleLocale = locale;
|
|
38
|
+
const url = locale
|
|
39
|
+
? `${googleIdentityURL}?hl=${encodeURIComponent(locale)}`
|
|
40
|
+
: googleIdentityURL;
|
|
41
|
+
googlePromise = appendScript(url, () => window.google, "Could not load Google Identity Services.").catch((error) => {
|
|
42
|
+
googlePromise = undefined;
|
|
43
|
+
googleLocale = undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
});
|
|
46
|
+
return googlePromise;
|
|
47
|
+
}
|
|
48
|
+
export function loadAppleIdentity(locale = "en_US") {
|
|
49
|
+
if (window.AppleID?.auth)
|
|
50
|
+
return Promise.resolve(window.AppleID.auth);
|
|
51
|
+
if (applePromise) {
|
|
52
|
+
if (locale !== appleLocale) {
|
|
53
|
+
return Promise.reject(new Error("Sign in with Apple has already been loaded with another locale."));
|
|
54
|
+
}
|
|
55
|
+
return applePromise;
|
|
56
|
+
}
|
|
57
|
+
appleLocale = locale;
|
|
58
|
+
const url = `${appleIdentityBaseURL}/${encodeURIComponent(locale)}/appleid.auth.js`;
|
|
59
|
+
applePromise = appendScript(url, () => window.AppleID?.auth, "Could not load Sign in with Apple.").catch((error) => {
|
|
60
|
+
applePromise = undefined;
|
|
61
|
+
appleLocale = undefined;
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
64
|
+
return applePromise;
|
|
65
|
+
}
|
|
66
|
+
/** @internal Test isolation for module-level third-party script promises. */
|
|
67
|
+
export function resetProviderSDKsForTests() {
|
|
68
|
+
googlePromise = undefined;
|
|
69
|
+
googleLocale = undefined;
|
|
70
|
+
applePromise = undefined;
|
|
71
|
+
appleLocale = undefined;
|
|
72
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AutharaReactClient } from "./contract.js";
|
|
2
|
+
import { type GoogleButtonConfiguration } from "./provider-sdks.js";
|
|
3
|
+
export type GoogleCredential = {
|
|
4
|
+
credential: string;
|
|
5
|
+
nonce: string;
|
|
6
|
+
selectBy?: string;
|
|
7
|
+
};
|
|
8
|
+
export type AppleAuthorization = {
|
|
9
|
+
code: string;
|
|
10
|
+
state: string;
|
|
11
|
+
};
|
|
12
|
+
type GoogleHandler = (credential: GoogleCredential) => void;
|
|
13
|
+
export declare class GoogleService {
|
|
14
|
+
private readonly client;
|
|
15
|
+
private optionsPromise?;
|
|
16
|
+
private sdkPromise?;
|
|
17
|
+
private handlers;
|
|
18
|
+
private activeState?;
|
|
19
|
+
constructor(client: AutharaReactClient);
|
|
20
|
+
render(element: HTMLElement, configuration: GoogleButtonConfiguration, locale: string | undefined, handler: GoogleHandler): Promise<() => void>;
|
|
21
|
+
private options;
|
|
22
|
+
private sdk;
|
|
23
|
+
}
|
|
24
|
+
export declare class AppleService {
|
|
25
|
+
private readonly client;
|
|
26
|
+
private renderOptionsPromise?;
|
|
27
|
+
private renderQueue;
|
|
28
|
+
constructor(client: AutharaReactClient);
|
|
29
|
+
render(element: HTMLElement, locale: string): Promise<void>;
|
|
30
|
+
authorize(locale: string): Promise<AppleAuthorization>;
|
|
31
|
+
private renderOptions;
|
|
32
|
+
private renderOfficialButton;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
package/dist/services.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { loadAppleIdentity, loadGoogleIdentity, } from "./provider-sdks.js";
|
|
2
|
+
let stateSequence = 0;
|
|
3
|
+
function nextState() {
|
|
4
|
+
stateSequence += 1;
|
|
5
|
+
return `authara-google-${stateSequence}`;
|
|
6
|
+
}
|
|
7
|
+
export class GoogleService {
|
|
8
|
+
constructor(client) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.handlers = new Map();
|
|
11
|
+
}
|
|
12
|
+
async render(element, configuration, locale, handler) {
|
|
13
|
+
const options = await this.options();
|
|
14
|
+
const google = await this.sdk(locale, options);
|
|
15
|
+
const state = nextState();
|
|
16
|
+
this.handlers.set(state, handler);
|
|
17
|
+
google.accounts.id.renderButton(element, {
|
|
18
|
+
...configuration,
|
|
19
|
+
locale,
|
|
20
|
+
state,
|
|
21
|
+
click_listener: () => {
|
|
22
|
+
this.activeState = state;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
this.handlers.delete(state);
|
|
27
|
+
if (this.activeState === state)
|
|
28
|
+
this.activeState = undefined;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
options() {
|
|
32
|
+
this.optionsPromise ?? (this.optionsPromise = this.client
|
|
33
|
+
.getGoogleLoginOptions()
|
|
34
|
+
.catch((error) => {
|
|
35
|
+
this.optionsPromise = undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}));
|
|
38
|
+
return this.optionsPromise;
|
|
39
|
+
}
|
|
40
|
+
sdk(locale, options) {
|
|
41
|
+
this.sdkPromise ?? (this.sdkPromise = loadGoogleIdentity(locale).then((google) => {
|
|
42
|
+
google.accounts.id.initialize({
|
|
43
|
+
client_id: options.client_id,
|
|
44
|
+
nonce: options.nonce,
|
|
45
|
+
callback: (response) => {
|
|
46
|
+
if (!response.credential)
|
|
47
|
+
return;
|
|
48
|
+
const state = response.state ?? this.activeState;
|
|
49
|
+
if (!state)
|
|
50
|
+
return;
|
|
51
|
+
this.handlers.get(state)?.({
|
|
52
|
+
credential: response.credential,
|
|
53
|
+
nonce: options.nonce,
|
|
54
|
+
selectBy: response.select_by,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return google;
|
|
59
|
+
}));
|
|
60
|
+
return this.sdkPromise;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function appleConfig(options) {
|
|
64
|
+
return {
|
|
65
|
+
clientId: options.client_id,
|
|
66
|
+
scope: "email",
|
|
67
|
+
redirectURI: options.redirect_uri,
|
|
68
|
+
state: options.state,
|
|
69
|
+
nonce: options.nonce,
|
|
70
|
+
usePopup: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export class AppleService {
|
|
74
|
+
constructor(client) {
|
|
75
|
+
this.client = client;
|
|
76
|
+
this.renderQueue = Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
async render(element, locale) {
|
|
79
|
+
const [auth, options] = await Promise.all([
|
|
80
|
+
loadAppleIdentity(locale),
|
|
81
|
+
this.renderOptions(),
|
|
82
|
+
]);
|
|
83
|
+
const rendering = this.renderQueue.then(() => this.renderOfficialButton(auth, options, element));
|
|
84
|
+
this.renderQueue = rendering.catch(() => undefined);
|
|
85
|
+
await rendering;
|
|
86
|
+
}
|
|
87
|
+
async authorize(locale) {
|
|
88
|
+
const [auth, options] = await Promise.all([
|
|
89
|
+
loadAppleIdentity(locale),
|
|
90
|
+
this.client.getAppleLoginOptions(),
|
|
91
|
+
]);
|
|
92
|
+
const result = await auth.signIn(appleConfig(options));
|
|
93
|
+
const { code, state } = result.authorization ?? {};
|
|
94
|
+
if (!code || !state) {
|
|
95
|
+
throw new Error("Apple did not return an authorization code and state.");
|
|
96
|
+
}
|
|
97
|
+
return { code, state };
|
|
98
|
+
}
|
|
99
|
+
renderOptions() {
|
|
100
|
+
this.renderOptionsPromise ?? (this.renderOptionsPromise = this.client
|
|
101
|
+
.getAppleLoginOptions()
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
this.renderOptionsPromise = undefined;
|
|
104
|
+
throw error;
|
|
105
|
+
}));
|
|
106
|
+
return this.renderOptionsPromise;
|
|
107
|
+
}
|
|
108
|
+
renderOfficialButton(auth, options, element) {
|
|
109
|
+
const existing = document.getElementById("appleid-signin");
|
|
110
|
+
existing?.removeAttribute("id");
|
|
111
|
+
element.id = "appleid-signin";
|
|
112
|
+
try {
|
|
113
|
+
auth.init(appleConfig(options));
|
|
114
|
+
auth.renderButton();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
element.removeAttribute("id");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authara/react",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Official React components for Authara browser authentication",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/authara-org/authara-react.git"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"check:contract": "node scripts/check-contract.mjs",
|
|
26
|
+
"check:format": "prettier --check \"src/**/*.{ts,tsx}\" \"contract/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
27
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\" \"contract/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
28
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\" \"contract/**/*.ts\"",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"prepublishOnly": "npm run check:contract && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@authara/browser": ">=0.6.0 <1.0.0",
|
|
34
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@authara/browser": "0.6.0",
|
|
38
|
+
"@eslint/js": "^9.39.2",
|
|
39
|
+
"@testing-library/react": "^16.3.2",
|
|
40
|
+
"@types/react": "^19.2.14",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
43
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
44
|
+
"eslint": "^9.39.2",
|
|
45
|
+
"happy-dom": "^20.4.0",
|
|
46
|
+
"prettier": "^3.8.1",
|
|
47
|
+
"react": "^19.1.1",
|
|
48
|
+
"react-dom": "^19.1.1",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"typescript-eslint": "^8.54.0",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
52
|
+
}
|
|
53
|
+
}
|