@synqauth/react-native 0.0.0 → 0.3.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 +3 -1
- package/dist/index.d.ts +97 -2
- package/dist/index.js +180 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Synq for React Native: sign-in through the system browser, with tokens in the de
|
|
|
4
4
|
|
|
5
5
|
Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at [docs.synq.build](https://docs.synq.build).
|
|
6
6
|
|
|
7
|
-
This package is at 0.x
|
|
7
|
+
This package is at 0.x, so a minor release can change its API: see its CHANGELOG.md.
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -32,6 +32,8 @@ const { signIn, signOut } = useAuth()
|
|
|
32
32
|
|
|
33
33
|
Sign-in opens the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never an embedded web view. The session, refresh token included, stays in the device's secure storage.
|
|
34
34
|
|
|
35
|
+
To sign people in on the app's own screens instead (own-UI sign-in, on for the app in the dashboard), use `useSignIn()`: `password.signIn({ email, password })`, `emailCode.send(email)` and `emailCode.verify(code)`, and the rest. Passkeys, and anything else only the hosted page can do, go on in the system browser with `continueInBrowser()`.
|
|
36
|
+
|
|
35
37
|
## License
|
|
36
38
|
|
|
37
39
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccountClient, AuthorizationUrlOptions, IdTokenClaims, Session, SynqClient, SynqError, SynqStorage } from "@synqauth/js";
|
|
1
|
+
import { AccountClient, AuthorizationUrlOptions, AvatarInfo, AvatarPerson, AvatarStyle, ChallengeMethod, ChallengeProblem, DeniedReason, IdTokenClaims, RedirectReason, Session, StartSignInOptions, SynqClient, SynqError, SynqStorage } from "@synqauth/js";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
//#region src/storage.d.ts
|
|
4
4
|
/** The part of `expo-secure-store` the SDK uses: pass the module itself. */
|
|
@@ -128,6 +128,26 @@ interface UseUser {
|
|
|
128
128
|
}
|
|
129
129
|
/** Who is signed in, and their account. */
|
|
130
130
|
export declare function useUser(): UseUser;
|
|
131
|
+
/** What `useAvatar()` answers: what the avatar is drawn from, and Synq's own default as SVG. */
|
|
132
|
+
interface UseAvatar extends AvatarInfo {
|
|
133
|
+
/**
|
|
134
|
+
* Synq's own default for them, as SVG markup (`avatarSvg`): draw it with
|
|
135
|
+
* react-native-svg's `SvgXml`, clipped to a circle, where no picture of
|
|
136
|
+
* theirs or of the app's own shows.
|
|
137
|
+
*/
|
|
138
|
+
readonly svg: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* What the avatar of `user` is drawn from (AERO-2704): the signed-in person
|
|
142
|
+
* by default, from their ID token, or anyone as Synq names them (`id` and
|
|
143
|
+
* `username` from the management API work too). Show `picture` when its
|
|
144
|
+
* `source` is one the app shows; else the app's own art from `seed`
|
|
145
|
+
* (`defaultPick` over its set); else `defaultPicture`; else `svg`. Null
|
|
146
|
+
* while there is nobody.
|
|
147
|
+
*/
|
|
148
|
+
export declare function useAvatar(user?: AvatarPerson | null, options?: {
|
|
149
|
+
style?: AvatarStyle;
|
|
150
|
+
}): UseAvatar | null;
|
|
131
151
|
/** What `useAuth()` answers: signing in and out, each answering `{ data, error }`. */
|
|
132
152
|
interface UseAuth {
|
|
133
153
|
readonly isLoaded: boolean;
|
|
@@ -142,4 +162,79 @@ interface UseAuth {
|
|
|
142
162
|
/** Signing in and out. */
|
|
143
163
|
export declare function useAuth(): UseAuth;
|
|
144
164
|
//#endregion
|
|
145
|
-
|
|
165
|
+
//#region src/sign-in.d.ts
|
|
166
|
+
/** Where a sign-in is: what the screen shows. */
|
|
167
|
+
type SignInStep = 'start' | 'code' | 'verify-email' | 'done';
|
|
168
|
+
/** What went wrong with the last step: branch on `code`, never on the English. */
|
|
169
|
+
interface SignInProblem {
|
|
170
|
+
/**
|
|
171
|
+
* Synq's problem with the step (`wrong_credentials`, `wrong_code`…), or
|
|
172
|
+
* the sign-in's own: `expired` (it timed out: start again), `refused`
|
|
173
|
+
* (this person can't sign in here: see `reason`), `unavailable` (Synq
|
|
174
|
+
* couldn't be reached, or failed).
|
|
175
|
+
*/
|
|
176
|
+
readonly code: ChallengeProblem | 'expired' | 'refused' | 'unavailable';
|
|
177
|
+
/** Why, when `refused`. */
|
|
178
|
+
readonly reason?: DeniedReason;
|
|
179
|
+
/** Synq's words for it, in English: show your own for `code` in another language. */
|
|
180
|
+
readonly message: string;
|
|
181
|
+
/** Seconds to wait first, when Synq said. */
|
|
182
|
+
readonly retryAfter?: number;
|
|
183
|
+
}
|
|
184
|
+
/** What only the hosted page can do for this sign-in, and what to take there. */
|
|
185
|
+
interface SignInHandover {
|
|
186
|
+
readonly reason: RedirectReason;
|
|
187
|
+
readonly method?: ChallengeMethod;
|
|
188
|
+
readonly loginHint?: string;
|
|
189
|
+
}
|
|
190
|
+
/** What `useSignIn()` answers: the sign-in's step, what went wrong, and each method's steps. */
|
|
191
|
+
interface UseSignIn {
|
|
192
|
+
readonly step: SignInStep;
|
|
193
|
+
/** Whether a step is on its way to Synq. */
|
|
194
|
+
readonly pending: boolean;
|
|
195
|
+
readonly problem: SignInProblem | null;
|
|
196
|
+
/** The address a code went to. */
|
|
197
|
+
readonly email: string | undefined;
|
|
198
|
+
/** Whether a new account's address may be left unverified for now. */
|
|
199
|
+
readonly canSkip: boolean;
|
|
200
|
+
/** Set when only the hosted page can go on: `continueInBrowser()` goes there. */
|
|
201
|
+
readonly handover: SignInHandover | null;
|
|
202
|
+
readonly password: {
|
|
203
|
+
readonly signIn: (input: {
|
|
204
|
+
email: string;
|
|
205
|
+
password: string;
|
|
206
|
+
}) => Promise<void>;
|
|
207
|
+
readonly signUp: (input: {
|
|
208
|
+
email: string;
|
|
209
|
+
password: string;
|
|
210
|
+
}) => Promise<void>;
|
|
211
|
+
};
|
|
212
|
+
readonly emailCode: {
|
|
213
|
+
readonly send: (email: string) => Promise<void>;
|
|
214
|
+
readonly verify: (code: string) => Promise<void>;
|
|
215
|
+
readonly resend: () => Promise<void>;
|
|
216
|
+
};
|
|
217
|
+
readonly verifyEmail: {
|
|
218
|
+
readonly verify: (code: string) => Promise<void>;
|
|
219
|
+
readonly resend: () => Promise<void>;
|
|
220
|
+
readonly skip: () => Promise<void>;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Goes on at the hosted page in the system browser, with the method and
|
|
224
|
+
* address Synq named, or these: a passkey, a token gate, a step-up. A
|
|
225
|
+
* closed browser is the error `cancelled`.
|
|
226
|
+
*/
|
|
227
|
+
readonly continueInBrowser: (options?: {
|
|
228
|
+
method?: string;
|
|
229
|
+
loginHint?: string;
|
|
230
|
+
}) => Promise<SynqResult<Session>>;
|
|
231
|
+
/** Drops the sign-in under way and goes back to the first step. */
|
|
232
|
+
readonly startOver: () => void;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* One sign-in on the app's own screens. The provider follows the session
|
|
236
|
+
* it ends with, as after `useAuth().signIn()`.
|
|
237
|
+
*/
|
|
238
|
+
export declare function useSignIn(options?: StartSignInOptions): UseSignIn;
|
|
239
|
+
//#endregion
|
|
240
|
+
export type { SecureStoreModule, SignInHandover, SignInProblem, SignInStep, SynqNative, SynqNativeOptions, SynqNativeProviderProps, SynqResult, UseAuth, UseAvatar, UseSession, UseSignIn, UseUser, WebBrowserModule };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SynqError, accountApiUrl, createAccountClient, createSynqClient, isSynqError } from "@synqauth/js";
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { SynqError, accountApiUrl, avatarOf, avatarSvg, createAccountClient, createSynqClient, isSynqError } from "@synqauth/js";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/storage.ts
|
|
5
5
|
/**
|
|
@@ -181,6 +181,7 @@ function SynqNativeProvider({ synq, issuer, children }) {
|
|
|
181
181
|
children
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
/** The provider's client and session, for this package's other hooks. */
|
|
184
185
|
function useSynq() {
|
|
185
186
|
const context = useContext(SynqContext);
|
|
186
187
|
if (!context) throw new Error("Synq hooks need a <SynqNativeProvider> above them");
|
|
@@ -212,6 +213,29 @@ function useUser() {
|
|
|
212
213
|
account
|
|
213
214
|
};
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* What the avatar of `user` is drawn from (AERO-2704): the signed-in person
|
|
218
|
+
* by default, from their ID token, or anyone as Synq names them (`id` and
|
|
219
|
+
* `username` from the management API work too). Show `picture` when its
|
|
220
|
+
* `source` is one the app shows; else the app's own art from `seed`
|
|
221
|
+
* (`defaultPick` over its set); else `defaultPicture`; else `svg`. Null
|
|
222
|
+
* while there is nobody.
|
|
223
|
+
*/
|
|
224
|
+
function useAvatar(user, options = {}) {
|
|
225
|
+
const { state } = useSynq();
|
|
226
|
+
const person = user ?? state.session?.claims ?? null;
|
|
227
|
+
return useMemo(() => {
|
|
228
|
+
if (!person) return null;
|
|
229
|
+
const avatar = avatarOf(person);
|
|
230
|
+
return {
|
|
231
|
+
...avatar,
|
|
232
|
+
svg: avatarSvg(avatar.seed, {
|
|
233
|
+
style: options.style,
|
|
234
|
+
initials: avatar.initials
|
|
235
|
+
})
|
|
236
|
+
};
|
|
237
|
+
}, [person, options.style]);
|
|
238
|
+
}
|
|
215
239
|
/** Signing in and out. */
|
|
216
240
|
function useAuth() {
|
|
217
241
|
const { synq, state } = useSynq();
|
|
@@ -249,8 +273,161 @@ function useAuth() {
|
|
|
249
273
|
signOut
|
|
250
274
|
};
|
|
251
275
|
}
|
|
276
|
+
/** Whatever a call threw, as a `SynqError`. */
|
|
252
277
|
function asSynqError(error) {
|
|
253
278
|
return isSynqError(error) ? error : new SynqError("network_error", String(error), { cause: error });
|
|
254
279
|
}
|
|
255
280
|
//#endregion
|
|
256
|
-
|
|
281
|
+
//#region src/sign-in.ts
|
|
282
|
+
/**
|
|
283
|
+
* One sign-in on the app's own screens. The provider follows the session
|
|
284
|
+
* it ends with, as after `useAuth().signIn()`.
|
|
285
|
+
*/
|
|
286
|
+
function useSignIn(options = {}) {
|
|
287
|
+
const { synq } = useSynq();
|
|
288
|
+
const auth = useAuth();
|
|
289
|
+
const flow = useRef(null);
|
|
290
|
+
const [step, setStep] = useState("start");
|
|
291
|
+
const [pending, setPending] = useState(0);
|
|
292
|
+
const [problem, setProblem] = useState(null);
|
|
293
|
+
const [email, setEmail] = useState(void 0);
|
|
294
|
+
const [canSkip, setCanSkip] = useState(false);
|
|
295
|
+
const [handover, setHandover] = useState(null);
|
|
296
|
+
const start = useRef(options);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
start.current = options;
|
|
299
|
+
});
|
|
300
|
+
const actions = useMemo(() => {
|
|
301
|
+
function show(result) {
|
|
302
|
+
switch (result.status) {
|
|
303
|
+
case "signed-in":
|
|
304
|
+
flow.current = null;
|
|
305
|
+
setStep("done");
|
|
306
|
+
setProblem(null);
|
|
307
|
+
return;
|
|
308
|
+
case "code-sent":
|
|
309
|
+
setStep("code");
|
|
310
|
+
setEmail(result.email);
|
|
311
|
+
setProblem(null);
|
|
312
|
+
return;
|
|
313
|
+
case "verify-email":
|
|
314
|
+
setStep("verify-email");
|
|
315
|
+
setEmail(result.email);
|
|
316
|
+
setCanSkip(result.canSkip);
|
|
317
|
+
setProblem(null);
|
|
318
|
+
return;
|
|
319
|
+
case "retry":
|
|
320
|
+
setProblem({
|
|
321
|
+
code: result.problem,
|
|
322
|
+
message: result.message,
|
|
323
|
+
...result.retryAfter === void 0 ? {} : { retryAfter: result.retryAfter }
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
case "hosted":
|
|
327
|
+
setHandover({
|
|
328
|
+
reason: result.reason,
|
|
329
|
+
...result.method ? { method: result.method } : {},
|
|
330
|
+
...result.loginHint ? { loginHint: result.loginHint } : {}
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
case "passkey":
|
|
334
|
+
setHandover({
|
|
335
|
+
reason: "passkey_unavailable",
|
|
336
|
+
method: "passkey"
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
case "refused":
|
|
340
|
+
flow.current = null;
|
|
341
|
+
setStep("start");
|
|
342
|
+
setProblem({
|
|
343
|
+
code: "refused",
|
|
344
|
+
reason: result.reason,
|
|
345
|
+
message: result.message
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function run(ask) {
|
|
351
|
+
flow.current ??= synq.client.startSignIn(start.current);
|
|
352
|
+
const current = flow.current;
|
|
353
|
+
setPending((n) => n + 1);
|
|
354
|
+
try {
|
|
355
|
+
show(await ask(current));
|
|
356
|
+
} catch (thrown) {
|
|
357
|
+
const error = asSynqError(thrown);
|
|
358
|
+
if (error.code === "invalid_session") {
|
|
359
|
+
flow.current = null;
|
|
360
|
+
setStep("start");
|
|
361
|
+
setProblem({
|
|
362
|
+
code: "expired",
|
|
363
|
+
message: error.message
|
|
364
|
+
});
|
|
365
|
+
} else if (error.code === "too_many_requests") setProblem({
|
|
366
|
+
code: "limited",
|
|
367
|
+
message: error.message,
|
|
368
|
+
...error.retryAfter === void 0 ? {} : { retryAfter: error.retryAfter }
|
|
369
|
+
});
|
|
370
|
+
else setProblem({
|
|
371
|
+
code: "unavailable",
|
|
372
|
+
message: error.message
|
|
373
|
+
});
|
|
374
|
+
} finally {
|
|
375
|
+
setPending((n) => n - 1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
password: {
|
|
380
|
+
signIn: (input) => run((f) => f.password.signIn(input)),
|
|
381
|
+
signUp: (input) => run((f) => f.password.signUp(input))
|
|
382
|
+
},
|
|
383
|
+
emailCode: {
|
|
384
|
+
send: (address) => run((f) => f.email.send(address)),
|
|
385
|
+
verify: (code) => run((f) => f.email.verify(code)),
|
|
386
|
+
resend: () => run((f) => f.email.resend())
|
|
387
|
+
},
|
|
388
|
+
verifyEmail: {
|
|
389
|
+
verify: (code) => run((f) => f.verifyEmail.verify(code)),
|
|
390
|
+
resend: () => run((f) => f.verifyEmail.resend()),
|
|
391
|
+
skip: () => run((f) => f.verifyEmail.skip())
|
|
392
|
+
},
|
|
393
|
+
startOver: () => {
|
|
394
|
+
flow.current = null;
|
|
395
|
+
setStep("start");
|
|
396
|
+
setProblem(null);
|
|
397
|
+
setEmail(void 0);
|
|
398
|
+
setCanSkip(false);
|
|
399
|
+
setHandover(null);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}, [synq]);
|
|
403
|
+
const continueInBrowser = useCallback(async (overrides = {}) => {
|
|
404
|
+
const method = overrides.method ?? handover?.method;
|
|
405
|
+
const loginHint = overrides.loginHint ?? handover?.loginHint ?? email;
|
|
406
|
+
const result = await auth.signIn({
|
|
407
|
+
...method ? { method } : {},
|
|
408
|
+
...loginHint ? { loginHint } : {}
|
|
409
|
+
});
|
|
410
|
+
if (!result.error) {
|
|
411
|
+
flow.current = null;
|
|
412
|
+
setHandover(null);
|
|
413
|
+
setStep("done");
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}, [
|
|
417
|
+
auth,
|
|
418
|
+
handover,
|
|
419
|
+
email
|
|
420
|
+
]);
|
|
421
|
+
return {
|
|
422
|
+
step,
|
|
423
|
+
pending: pending > 0,
|
|
424
|
+
problem,
|
|
425
|
+
email,
|
|
426
|
+
canSkip,
|
|
427
|
+
handover,
|
|
428
|
+
...actions,
|
|
429
|
+
continueInBrowser
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
export { CHUNK_SIZE, SynqNativeProvider, createSynqNative, secureStorage, useAuth, useAvatar, useSession, useSignIn, useUser };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synqauth/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Synq for React Native: sign-in through the system browser, with tokens in the device's secure storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@synqauth/js": "0.
|
|
30
|
+
"@synqauth/js": "0.3.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=19.0.0"
|