@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 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
- # Temporary Holding Version
1
+ # @authara/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 {};
@@ -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>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function isProviderUnavailable(error: unknown): boolean;
2
+ export declare function toError(error: unknown, fallback: string): Error;
3
+ export declare function isAppleCancellation(error: unknown): boolean;
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
+ }
@@ -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,4 @@
1
+ export { AppleButton } from "./AppleButton.js";
2
+ export { GoogleButton } from "./GoogleButton.js";
3
+ export { AutharaProvider } from "./context.js";
4
+ export { isAppleCancellation, isProviderUnavailable } from "./errors.js";
@@ -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 {};
@@ -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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }