@authara/react 0.0.1-trusted.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # @authara/react
2
2
 
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).
3
+ Official React buttons for complete Authara Google and Apple authentication
4
+ flows. The components load the provider SDK, obtain the provider proof, prepare
5
+ CSRF, call the generated Authara browser client, and return a typed result.
8
6
 
9
7
  ## Installation
10
8
 
@@ -12,117 +10,373 @@ in [`@authara/browser`](https://www.npmjs.com/package/@authara/browser).
12
10
  npm install @authara/react @authara/browser react
13
11
  ```
14
12
 
15
- ## Usage
13
+ ## Setup
16
14
 
17
- Create one browser client and place one provider near the root of the page:
15
+ Create one browser client and one provider near the root of the page:
18
16
 
19
17
  ```tsx
20
18
  import { AutharaBrowserClient } from "@authara/browser";
21
- import {
22
- AppleButton,
23
- AutharaProvider,
24
- GoogleButton
25
- } from "@authara/react";
19
+ import { AutharaProvider } from "@authara/react";
26
20
 
27
- const authara = new AutharaBrowserClient();
21
+ const authara = new AutharaBrowserClient({
22
+ automaticRefresh: { audience: "app" },
23
+ });
28
24
 
29
- export function Login() {
25
+ export function App() {
30
26
  return (
31
27
  <AutharaProvider client={authara}>
28
+ <Routes />
29
+ </AutharaProvider>
30
+ );
31
+ }
32
+ ```
33
+
34
+ The provider owns no user or session state. It coordinates Authara's generated
35
+ client and the page-global Google and Apple SDKs.
36
+
37
+ ## Login
38
+
39
+ ```tsx
40
+ import { AppleButton, GoogleButton } from "@authara/react";
41
+
42
+ export function Login() {
43
+ return (
44
+ <div>
32
45
  <GoogleButton
33
- onCredential={async ({ credential, nonce }) => {
34
- await authara.loginWithGoogle({
35
- audience: "app",
36
- body: { credential, nonce }
37
- });
38
- }}
39
- onError={console.error}
46
+ action="login"
47
+ audience="app"
48
+ variant="text"
49
+ size="large"
50
+ onSuccess={({ session }) => setCurrentUser(session.user)}
51
+ onError={handleAuthError}
40
52
  />
41
53
 
42
54
  <AppleButton
43
- onAuthorization={async ({ code, state }) => {
44
- await authara.loginWithApple({
45
- audience: "app",
46
- body: { code, state }
47
- });
48
- }}
49
- onError={console.error}
55
+ action="login"
56
+ audience="app"
57
+ variant="text"
58
+ size="large"
59
+ onSuccess={({ session }) => setCurrentUser(session.user)}
60
+ onError={handleAuthError}
61
+ onCancel={() => setMessage("Sign in was cancelled.")}
50
62
  />
51
- </AutharaProvider>
63
+ </div>
52
64
  );
53
65
  }
54
66
  ```
55
67
 
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.
68
+ `action="login"` supports the `app`, `admin`, and `operator` audiences. The
69
+ default action is `login` and the default audience is `app`.
70
+
71
+ ## Link a provider
59
72
 
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.
73
+ ```tsx
74
+ <GoogleButton
75
+ action="link"
76
+ onSuccess={() => refreshAccount()}
77
+ onError={handleAuthError}
78
+ />
79
+
80
+ <AppleButton
81
+ action="link"
82
+ onSuccess={() => refreshAccount()}
83
+ onError={handleAuthError}
84
+ />
85
+ ```
63
86
 
64
- ## Provider availability
87
+ Linking uses the current Authara session and therefore does not accept an
88
+ audience. Authara may return `recent_authentication_required` when the session
89
+ needs a fresh proof.
65
90
 
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`.
91
+ ## Reauthenticate
69
92
 
70
93
  ```tsx
71
- <GoogleButton
72
- fallback={<p>Google sign-in is not configured.</p>}
73
- onUnavailable={() => reportProviderUnavailable("google")}
74
- onCredential={handleGoogleCredential}
94
+ <AppleButton
95
+ action="reauthenticate"
96
+ authenticationChallengeId={challenge.id}
97
+ onSuccess={() => retrySensitiveAction()}
98
+ onError={handleAuthError}
75
99
  />
76
100
  ```
77
101
 
78
- Apple user cancellation calls `onCancel`; it is not reported through
79
- `onError`.
102
+ TypeScript requires `authenticationChallengeId` for reauthentication and
103
+ rejects `audience` for both linking and reauthentication.
104
+
105
+ ## Account collisions
106
+
107
+ A login with an email owned by another sign-in method returns
108
+ `account_link_required`. The error contains the pending provider, a short-lived
109
+ link ID, and the methods that can prove ownership. Render the matching provider
110
+ button with `action="recover"`:
111
+
112
+ ```tsx
113
+ import type {
114
+ AutharaAccountRecoveryLink,
115
+ AutharaButtonError,
116
+ } from "@authara/react";
117
+
118
+ const [recovery, setRecovery] = useState<AutharaAccountRecoveryLink>();
119
+
120
+ function handleLoginError(error: AutharaButtonError) {
121
+ if (error.code === "account_link_required" && error.recovery) {
122
+ setRecovery(error.recovery);
123
+ return;
124
+ }
125
+ showMessage(error.message);
126
+ }
127
+
128
+ {recovery?.proof_methods.includes("google") && (
129
+ <GoogleButton
130
+ action="recover"
131
+ recoveryLinkId={recovery.link_id}
132
+ onSuccess={({ session }) => setCurrentUser(session.user)}
133
+ onError={handleLoginError}
134
+ />
135
+ )}
136
+
137
+ {recovery?.proof_methods.includes("apple") && (
138
+ <AppleButton
139
+ action="recover"
140
+ recoveryLinkId={recovery.link_id}
141
+ onSuccess={({ session }) => setCurrentUser(session.user)}
142
+ onError={handleLoginError}
143
+ />
144
+ )}
145
+ ```
146
+
147
+ This is symmetric: an Apple collision can be proved with an existing Google
148
+ identity, and a Google collision can be proved with an existing Apple identity.
149
+ Authara never links accounts from an email match alone.
150
+
151
+ ## Errors
152
+
153
+ `onError` receives `AutharaButtonError`. It identifies the failing layer and
154
+ preserves structured Authara metadata:
155
+
156
+ ```tsx
157
+ import type { AutharaButtonError } from "@authara/react";
158
+
159
+ function handleAuthError(error: AutharaButtonError) {
160
+ switch (error.code) {
161
+ case "recent_authentication_required":
162
+ openReauthentication(error.authenticationChallenge);
163
+ return;
164
+ case "auth_method_already_linked":
165
+ showMessage("That provider is already linked.");
166
+ return;
167
+ case "unauthorized":
168
+ navigate("/login");
169
+ return;
170
+ default:
171
+ showMessage(error.message);
172
+ }
173
+ }
174
+ ```
175
+
176
+ Useful fields are:
177
+
178
+ - `source`: `authara`, `provider`, `configuration`, or `consumer`
179
+ - `status`: the HTTP status when Authara responded
180
+ - `code`: the stable Authara error code, or `unknown_authara_error`
181
+ - `serverCode`: an unrecognized server code when `code` is
182
+ `unknown_authara_error`
183
+ - `authenticationChallenge`: the challenge returned with HTTP 428
184
+ - `reauthenticateUrl`: Authara's optional reauthentication URL
185
+ - `recovery`: typed account-collision recovery details returned with HTTP 409
186
+ - `originalError`: the original provider, SDK, or callback failure
187
+
188
+ The selected `action` narrows the error-code type passed to `onError`. These
189
+ unions are generated from Core's `x-authara-error-codes` declarations:
190
+
191
+ - login adds `account_link_required`
192
+ - link adds `auth_method_already_linked` and
193
+ `recent_authentication_required`
194
+ - reauthentication adds `invalid_authentication_challenge`
195
+ - recovery adds the provider-link and invitation completion errors
196
+ - every action includes its declared authorization, request, availability, and
197
+ internal codes, plus the local CSRF and unknown-code fallbacks
198
+
199
+ Apple popup cancellation calls `onCancel`, not `onError`. A provider disabled
200
+ in Authara renders `fallback` and calls `onUnavailable`.
80
201
 
81
202
  ## Appearance
82
203
 
83
- `GoogleButton` exposes the official Google button configuration: `type`,
84
- `theme`, `size`, `text`, `shape`, `logo_alignment`, `width`, and `locale`.
204
+ Both components share the simple properties:
85
205
 
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.
206
+ ```tsx
207
+ <AppleButton
208
+ action="login"
209
+ variant="icon"
210
+ size="small"
211
+ />
89
212
 
90
- ## Contract synchronization
213
+ <GoogleButton
214
+ action="login"
215
+ variant="text"
216
+ size="large"
217
+ mode="system"
218
+ />
219
+ ```
220
+
221
+ - `variant`: `text` or `icon`
222
+ - `size`: `small`, `medium`, or `large`
223
+ - `label`: `sign-in`, `sign-up`, or `continue`
224
+
225
+ Text-button heights are `30px`, `36px`, and `44px`. Icon-only buttons are
226
+ larger at `36px`, `44px`, and `52px` for the same three size names.
91
227
 
92
- The components never contain Authara route strings. They consume the generated
93
- `AutharaBrowserClient` operation methods:
228
+ `mode` accepts `"light"`, `"dark"`, or `"system"` and defaults to `"light"`.
229
+ `system` follows live changes to `prefers-color-scheme`. Light mode uses the
230
+ black Apple button and Google's light preset. Dark mode uses the white Apple
231
+ button and Google's dark preset. Apple is borderless in both modes. Explicit
232
+ Apple `color` or `border`, or Google `theme` or `border`, properties take
233
+ precedence over these defaults.
94
234
 
95
- - `getGoogleLoginOptions()`
96
- - `getAppleLoginOptions()`
235
+ Google additionally exposes `theme`, `border`, `shape`, `logoAlignment`, and
236
+ `width`.
237
+ Apple exposes `color`, `border`, `borderRadius`, `align`, and `width`.
238
+ Both providers accept `buttonText` to localize or otherwise override the
239
+ visible label and the accessible name of icon-only buttons.
240
+ Text and icon variants both default to a `12px` corner radius. Apple's
241
+ `borderRadius` accepts its supported `0`–`50` range; Google's `shape` can still
242
+ select `pill`, `circle`, `square`, or `rectangular` explicitly.
243
+ Both providers use an Authara-rendered, centered logo-and-label structure.
244
+ Google's `light`, `dark`, and `neutral` themes use Google's required colors,
245
+ spacing, and typography. Make Google Sans Medium available in your application
246
+ as required by Google's branding guidelines; this package does not inject a
247
+ font download or redistribute Google's font. The provider SDKs still own the
248
+ underlying credential and authorization flows. Google's visible control is a
249
+ real native button; it starts the popup authorization-code flow directly and
250
+ does not place an invisible provider button over Authara's presentation.
97
251
 
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.
252
+ ## Advanced provider-proof components
101
253
 
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:
254
+ Applications with a custom multi-step operation can use the lower-level
255
+ components:
106
256
 
107
- ```text
108
- Authara Core OpenAPI release
109
- -> generated @authara/browser release
110
- -> @authara/react contract check and dependency PR
257
+ ```tsx
258
+ <GoogleCredentialButton onCredential={handleGoogleProof} />
259
+ <AppleCredentialButton onAuthorization={handleAppleProof} />
111
260
  ```
112
261
 
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.
262
+ Most applications should use `GoogleButton` and `AppleButton`; the lower-level
263
+ components deliberately expose Google's one-time code and browser origin or
264
+ Apple's code and state.
265
+
266
+ ## Real local playground
267
+
268
+ The Vite playground uses the real Google and Apple scripts and sends real HTTP
269
+ requests to Authara. It has no provider or API mocks.
270
+
271
+ For fast component development, start an Authara gateway with Google and/or
272
+ Apple configured, then run:
273
+
274
+ ```bash
275
+ npm ci
276
+ npm run dev
277
+ ```
278
+
279
+ Core reads Google configuration from `AUTHARA_OAUTH_GOOGLE_CLIENT_ID` and
280
+ `AUTHARA_OAUTH_GOOGLE_CLIENT_SECRET`. Keep the secret on Core; React never
281
+ receives it. Apple uses `AUTHARA_OAUTH_APPLE_CLIENT_ID`, `AUTHARA_OAUTH_APPLE_TEAM_ID`,
282
+ `AUTHARA_OAUTH_APPLE_KEY_ID`, `AUTHARA_OAUTH_APPLE_PRIVATE_KEY_BASE64`, and the
283
+ Apple token-key settings documented by Authara Core.
284
+
285
+ By default, Vite proxies `/auth` to `http://localhost:3001`. Override the Core
286
+ or gateway address when needed:
287
+
288
+ ```bash
289
+ AUTHARA_PLAYGROUND_PROXY_TARGET=http://localhost:3000 npm run dev
290
+ ```
291
+
292
+ Alternatively, set `VITE_AUTHARA_BASE_URL` to call an Authara origin directly.
293
+ That origin must allow the playground origin and credentialed browser requests.
294
+ The same-origin proxy is usually simpler.
295
+
296
+ To rehearse the way an operator deploys the packages, use the version-pinned
297
+ Compose stack instead. It builds the package first, builds the playground
298
+ against the package's public `dist` exports, and serves it through Authara
299
+ Gateway alongside released Core, migrations, PostgreSQL, and Redis images. A
300
+ `cloudflared` container publishes that gateway through the named tunnel:
301
+
302
+ ```bash
303
+ cp .env.example .env
304
+ # Paste the tunnel's eyJ... token into CLOUDFLARE_TUNNEL_TOKEN in .env.
305
+ docker compose -f docker-compose.playground.yaml up --build
306
+ ```
307
+
308
+ While testing unreleased Core contract changes from this workspace, add the
309
+ local override. It keeps the operator-style stack but builds the neighboring
310
+ `authara-core` checkout instead of pulling the released Core image:
311
+
312
+ ```bash
313
+ docker compose \
314
+ -f docker-compose.playground.yaml \
315
+ -f docker-compose.playground.local.yaml \
316
+ up --build
317
+ ```
318
+
319
+ The Cloudflare route for `playground.authara.org` must use
320
+ `http://gateway:3000` as its service URL because the connector runs inside the
321
+ Compose network. Open `https://playground.authara.org`; `http://localhost:3000`
322
+ remains available for direct local debugging.
323
+
324
+ After configuring Google and Apple, fill their values in `.env` and set
325
+ `AUTHARA_OAUTH_PROVIDERS=google,apple`. Until then, leave the provider list
326
+ empty; the playground displays the disabled-provider fallbacks instead of
327
+ attempting OAuth. Never commit `.env` or the tunnel token.
328
+
329
+ The Compose file defaults to the exact Core and migrations versions recorded
330
+ for this React contract; override `AUTHARA_CORE_VERSION` and
331
+ `AUTHARA_MIGRATIONS_VERSION` only when deliberately testing another released
332
+ pair.
333
+
334
+ For the Compose deployment, configure both providers with the public origin
335
+ `https://playground.authara.org`. The Google redirect URI is
336
+ `https://playground.authara.org/auth/oauth/google/callback`; the Apple domain is
337
+ `playground.authara.org` and its return URL is
338
+ `https://playground.authara.org/auth/oauth/apple/callback`.
339
+
340
+ The playground covers:
341
+
342
+ - real login for `app`, `admin`, and `operator`
343
+ - linking Google or Apple to the current account
344
+ - signed-out Google-to-Apple and Apple-to-Google account-collision recovery
345
+ - Google or Apple reauthentication with a challenge UUID
346
+ - text and icon variants in all three sizes
347
+ - disabled-provider fallbacks, cancellation, and structured errors
348
+ - current-session inspection without displaying access or refresh tokens
349
+
350
+ “Request challenge” calls Authara's recent-authentication check. A fresh session
351
+ returns success without a challenge; an old session returns HTTP 428 and the
352
+ playground fills the challenge UUID automatically.
353
+
354
+ To rehearse account-collision recovery, use “Sign out,” select “Recover account
355
+ collision,” and attempt normal Google or Apple login with an email already
356
+ owned by the other provider. Authara's `409 account_link_required` response
357
+ already contains the pending link, so the playground immediately renders the
358
+ available existing-account proof buttons. It creates and consumes Core's real
359
+ ten-minute `link_id`; it does not simulate either provider response.
360
+
361
+ ## Contract synchronization
362
+
363
+ The published components contain no Authara route strings. They call methods on
364
+ the generated `@authara/browser` client. The package's compatibility manifest
365
+ and synchronization workflow bind each React release to an immutable browser
366
+ SDK release and its Authara Core OpenAPI provenance.
116
367
 
117
- ## Design guarantees
368
+ Each Browser release triggers a compatibility PR. The workflow waits for npm
369
+ registry propagation, verifies the Browser and Core commits and OpenAPI hash,
370
+ updates the exact Browser development dependency, and regenerates the action
371
+ error unions from Core. Route/type-compatible updates may auto-merge. A changed
372
+ error union deliberately requires human review so component mechanics and the
373
+ documented application handling can be updated when necessary.
118
374
 
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
375
+ Provider operations are discovered from Core's `get…LoginOptions` operations,
376
+ so Microsoft error declarations will enter the generated unions automatically.
377
+ A Microsoft button still needs its provider-SDK adapter and branded component;
378
+ it can use the same `login`, `link`, `reauthenticate`, and `recover` action
379
+ contract without a separate collision design.
126
380
 
127
381
  ## License
128
382
 
@@ -1,17 +1,36 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from "react";
2
- import type { MaybePromise } from "./contract.js";
1
+ import { type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from "react";
2
+ import type { AutharaActionProps, MaybePromise } from "./contract.js";
3
3
  import type { AppleAuthorization } from "./services.js";
4
- export interface AppleButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError"> {
4
+ import { type AutharaColorMode } from "./color-mode.js";
5
+ type AppleButtonType = "sign-in" | "continue" | "sign-up";
6
+ export interface AppleCredentialButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError"> {
5
7
  disabled?: boolean;
6
8
  locale?: string;
7
9
  color?: "black" | "white";
8
10
  border?: boolean;
9
- type?: "sign-in" | "continue" | "sign-up";
11
+ buttonText?: string;
12
+ type?: AppleButtonType;
10
13
  mode?: "center-align" | "left-align" | "logo-only";
14
+ /** Apple supports a border radius from 0 through 50 points. */
15
+ borderRadius?: number;
11
16
  fallback?: ReactNode;
12
17
  onAuthorization: (authorization: AppleAuthorization) => MaybePromise<void>;
13
18
  onCancel?: () => void;
14
19
  onError?: (error: Error) => void;
15
20
  onUnavailable?: () => void;
16
21
  }
17
- export declare function AppleButton({ disabled, locale, color, border, type, mode, fallback, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }: AppleButtonProps): React.JSX.Element | null;
22
+ export declare function AppleCredentialButton({ disabled, locale, color, border, buttonText, type, mode, borderRadius, fallback, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }: AppleCredentialButtonProps): React.JSX.Element | null;
23
+ export type AppleButtonVariant = "icon" | "text";
24
+ export type AppleButtonLabel = "continue" | "sign-in" | "sign-up";
25
+ type AppleButtonVisualProps = Omit<AppleCredentialButtonProps, "mode" | "onAuthorization" | "onCancel" | "onError" | "type"> & {
26
+ variant?: AppleButtonVariant;
27
+ size?: "small" | "medium" | "large";
28
+ label?: AppleButtonLabel;
29
+ align?: "center" | "left";
30
+ mode?: AutharaColorMode;
31
+ width?: CSSProperties["width"];
32
+ onCancel?: () => void;
33
+ };
34
+ export type AppleButtonProps = AppleButtonVisualProps & AutharaActionProps<"apple">;
35
+ export declare function AppleButton(props: AppleButtonProps): React.JSX.Element | null;
36
+ export {};
@@ -1,8 +1,26 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState, } from "react";
3
- import { isAppleCancellation, isProviderUnavailable, toError, } from "./errors.js";
3
+ import { resolveAction, runAppleAction } from "./actions.js";
4
+ import { isAppleCancellation, isProviderUnavailable, toAutharaButtonError, toError, } from "./errors.js";
4
5
  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
+ import { useResolvedAutharaColorMode, } from "./color-mode.js";
7
+ function appleButtonText(type) {
8
+ switch (type) {
9
+ case "sign-in":
10
+ return "Sign in with Apple";
11
+ case "sign-up":
12
+ return "Sign up with Apple";
13
+ case "continue":
14
+ return "Continue with Apple";
15
+ }
16
+ }
17
+ function AppleLogo() {
18
+ return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 814 1000", width: "16", height: "20", focusable: "false", children: _jsx("path", { fill: "currentColor", d: "M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z" }) }));
19
+ }
20
+ export function AppleCredentialButton({ disabled = false, locale = "en_US", color = "black", border = false, buttonText, type = "continue", mode = "center-align", borderRadius = 12, fallback = null, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }) {
21
+ if (!Number.isFinite(borderRadius) || borderRadius < 0 || borderRadius > 50) {
22
+ throw new RangeError("Apple button borderRadius must be between 0 and 50.");
23
+ }
6
24
  const { apple } = useAutharaServices();
7
25
  const targetRef = useRef(null);
8
26
  const authorizationRef = useRef(onAuthorization);
@@ -40,7 +58,7 @@ export function AppleButton({ disabled = false, locale = "en_US", color = "black
40
58
  return () => {
41
59
  active = false;
42
60
  };
43
- }, [apple, border, color, locale, mode, type]);
61
+ }, [apple, border, borderRadius, color, locale, mode, type]);
44
62
  const beginAuthorization = (event) => {
45
63
  event.preventDefault();
46
64
  event.stopPropagation();
@@ -62,10 +80,110 @@ export function AppleButton({ disabled = false, locale = "en_US", color = "black
62
80
  if (!available)
63
81
  return _jsx(_Fragment, { children: fallback });
64
82
  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: {
83
+ const label = buttonText ?? appleButtonText(type);
84
+ return (_jsxs("div", { ...divProps, "aria-busy": busy || undefined, "aria-disabled": inactive || undefined, "data-authara-provider": "apple", "data-authara-ready": ready ? "true" : "false", onClickCapture: beginAuthorization, style: {
85
+ position: "relative",
86
+ display: "inline-block",
66
87
  width: "100%",
67
88
  height: 44,
89
+ overflow: "hidden",
90
+ borderRadius,
91
+ cursor: !ready ? "wait" : inactive ? "not-allowed" : "pointer",
92
+ lineHeight: 0,
93
+ opacity: inactive ? 0.62 : 1,
94
+ transition: "opacity 150ms ease",
95
+ verticalAlign: "middle",
96
+ WebkitTapHighlightColor: "transparent",
97
+ ...style,
98
+ pointerEvents: !ready || inactive ? "none" : style?.pointerEvents,
99
+ }, children: [_jsx("div", { ref: targetRef, "aria-hidden": "true", "data-color": color, "data-border": String(border), "data-border-radius": borderRadius, "data-width": "100%", "data-height": "100%", "data-type": type, "data-mode": mode, style: {
100
+ position: "absolute",
101
+ inset: 0,
102
+ overflow: "hidden",
103
+ borderRadius,
104
+ opacity: 0,
105
+ pointerEvents: "none",
106
+ } }), _jsxs("button", { type: "button", disabled: !ready || inactive, "aria-label": mode === "logo-only" ? label : undefined, "data-authara-apple-presentation": true, style: {
107
+ position: "absolute",
108
+ inset: 0,
109
+ display: "flex",
110
+ width: "100%",
111
+ height: "100%",
112
+ alignItems: "center",
113
+ justifyContent: "center",
114
+ gap: mode === "logo-only" || mode === "left-align" ? 0 : 10,
115
+ padding: mode === "logo-only" ? 0 : "0 14px",
116
+ overflow: "hidden",
117
+ border: "1px solid",
118
+ borderColor: border
119
+ ? color === "black"
120
+ ? "#000"
121
+ : "#9aa0a6"
122
+ : "transparent",
123
+ borderRadius,
124
+ background: color === "black" ? "#000" : "#fff",
125
+ boxShadow: "none",
126
+ color: color === "black" ? "#fff" : "#202124",
127
+ cursor: inactive ? "not-allowed" : ready ? "pointer" : "wait",
128
+ fontFamily: "Arial, sans-serif",
129
+ fontSize: 14,
130
+ fontWeight: 500,
131
+ lineHeight: 1,
132
+ whiteSpace: "nowrap",
133
+ }, children: [_jsx("span", { "aria-hidden": "true", style: {
134
+ display: "inline-flex",
135
+ width: 20,
136
+ height: 20,
137
+ flex: "0 0 20px",
138
+ alignItems: "center",
139
+ justifyContent: "center",
140
+ position: mode === "left-align" ? "absolute" : "static",
141
+ left: mode === "left-align" ? 14 : undefined,
142
+ }, children: _jsx(AppleLogo, {}) }), mode !== "logo-only" && _jsx("span", { children: label })] })] }));
143
+ }
144
+ const appleButtonHeights = {
145
+ small: 30,
146
+ medium: 36,
147
+ large: 44,
148
+ };
149
+ const appleIconButtonSizes = {
150
+ small: 36,
151
+ medium: 44,
152
+ large: 52,
153
+ };
154
+ function appleType(label) {
155
+ return label;
156
+ }
157
+ export function AppleButton(props) {
158
+ const { client } = useAutharaServices();
159
+ const internal = props;
160
+ const { action = "login", audience, authenticationChallengeId, recoveryLinkId, onSuccess, onError, onCancel, variant = "text", size = "large", label = action === "login" ? "sign-in" : "continue", align = "center", mode = "light", width, color, borderRadius, style, ...buttonProps } = internal;
161
+ const height = variant === "icon" ? appleIconButtonSizes[size] : appleButtonHeights[size];
162
+ const resolvedMode = useResolvedAutharaColorMode(mode);
163
+ return (_jsx(AppleCredentialButton, { ...buttonProps, color: color ?? (resolvedMode === "dark" ? "white" : "black"), type: appleType(label), mode: variant === "icon" ? "logo-only" : `${align}-align`, borderRadius: borderRadius, style: {
164
+ width: width ?? (variant === "icon" ? height : "100%"),
165
+ height,
68
166
  ...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%" } }) }));
167
+ }, onAuthorization: async (proof) => {
168
+ let result;
169
+ let selected;
170
+ try {
171
+ selected = resolveAction(action, audience, authenticationChallengeId, recoveryLinkId);
172
+ }
173
+ catch (error) {
174
+ throw toAutharaButtonError(error, "The Apple button action is not configured correctly.", "configuration");
175
+ }
176
+ try {
177
+ result = await runAppleAction(client, selected, proof);
178
+ }
179
+ catch (error) {
180
+ throw toAutharaButtonError(error, `Apple ${action} failed.`, "authara");
181
+ }
182
+ try {
183
+ await onSuccess?.(result);
184
+ }
185
+ catch (error) {
186
+ throw toAutharaButtonError(error, "The Apple success callback failed.", "consumer");
187
+ }
188
+ }, onCancel: onCancel, onError: (error) => onError?.(toAutharaButtonError(error, "Apple authentication failed.", "provider")) }));
71
189
  }