@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 +331 -77
- package/dist/AppleButton.d.ts +24 -5
- package/dist/AppleButton.js +125 -7
- package/dist/GoogleButton.d.ts +17 -3
- package/dist/GoogleButton.js +162 -28
- package/dist/actions.d.ts +17 -0
- package/dist/actions.js +88 -0
- package/dist/color-mode.d.ts +3 -0
- package/dist/color-mode.js +20 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.js +1 -0
- package/dist/contract.d.ts +99 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +96 -0
- package/dist/generated-error-codes.d.ts +8 -0
- package/dist/generated-error-codes.js +42 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/provider-sdks.d.ts +20 -15
- package/dist/provider-sdks.js +6 -3
- package/dist/services.d.ts +3 -7
- package/dist/services.js +25 -41
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# @authara/react
|
|
2
2
|
|
|
3
|
-
Official React
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
##
|
|
13
|
+
## Setup
|
|
16
14
|
|
|
17
|
-
Create one browser client and
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
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
|
-
</
|
|
63
|
+
</div>
|
|
52
64
|
);
|
|
53
65
|
}
|
|
54
66
|
```
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
disabled. The corresponding button then renders `fallback` (which defaults to
|
|
68
|
-
`null`) and calls `onUnavailable`.
|
|
91
|
+
## Reauthenticate
|
|
69
92
|
|
|
70
93
|
```tsx
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
<AppleButton
|
|
95
|
+
action="reauthenticate"
|
|
96
|
+
authenticationChallengeId={challenge.id}
|
|
97
|
+
onSuccess={() => retrySensitiveAction()}
|
|
98
|
+
onError={handleAuthError}
|
|
75
99
|
/>
|
|
76
100
|
```
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
`
|
|
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
|
-
|
|
84
|
-
`theme`, `size`, `text`, `shape`, `logo_alignment`, `width`, and `locale`.
|
|
204
|
+
Both components share the simple properties:
|
|
85
205
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
206
|
+
```tsx
|
|
207
|
+
<AppleButton
|
|
208
|
+
action="login"
|
|
209
|
+
variant="icon"
|
|
210
|
+
size="small"
|
|
211
|
+
/>
|
|
89
212
|
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
`
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-> @authara/react contract check and dependency PR
|
|
257
|
+
```tsx
|
|
258
|
+
<GoogleCredentialButton onCredential={handleGoogleProof} />
|
|
259
|
+
<AppleCredentialButton onAuthorization={handleAppleProof} />
|
|
111
260
|
```
|
|
112
261
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
package/dist/AppleButton.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 {};
|
package/dist/AppleButton.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import { Fragment as _Fragment,
|
|
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 {
|
|
3
|
+
import { resolveAction, runAppleAction } from "./actions.js";
|
|
4
|
+
import { isAppleCancellation, isProviderUnavailable, toAutharaButtonError, toError, } from "./errors.js";
|
|
4
5
|
import { useAutharaServices } from "./context.js";
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
}
|