@synqauth/react 0.0.0 → 0.2.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 +237 -6
- package/dist/index.js +1082 -9
- package/dist/styles.css +25 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -39,7 +39,80 @@ const DEFAULT_MESSAGES = {
|
|
|
39
39
|
renameWallet: (name) => `Rename ${name}`,
|
|
40
40
|
makePrimary: "Make primary",
|
|
41
41
|
makeWalletPrimary: (name) => `Make ${name} primary`,
|
|
42
|
-
walletName: (address) => `Name for ${address}
|
|
42
|
+
walletName: (address) => `Name for ${address}`,
|
|
43
|
+
apiKeys: "API keys",
|
|
44
|
+
noApiKeys: "No API keys yet.",
|
|
45
|
+
newApiKey: "New API key",
|
|
46
|
+
api: "API",
|
|
47
|
+
keyScopes: "What it may do",
|
|
48
|
+
expires: "Expires",
|
|
49
|
+
never: "Never",
|
|
50
|
+
inDays: (days) => `In ${days} days`,
|
|
51
|
+
makeKey: "Make key",
|
|
52
|
+
makingKey: "Making…",
|
|
53
|
+
revoke: "Revoke",
|
|
54
|
+
revokeKey: (name) => `Revoke ${name}`,
|
|
55
|
+
revoked: "Revoked",
|
|
56
|
+
expired: "Expired",
|
|
57
|
+
lastUsed: (when) => `Last used ${when}`,
|
|
58
|
+
neverUsed: "Never used",
|
|
59
|
+
expiresOn: (when) => `Expires ${when}`,
|
|
60
|
+
copyKeyNow: "Copy this key now: you won’t see it again.",
|
|
61
|
+
copy: "Copy",
|
|
62
|
+
copied: "Copied",
|
|
63
|
+
done: "Done",
|
|
64
|
+
keyNeedsFreshSignIn: "Making a key needs a recent sign-in.",
|
|
65
|
+
signInAgain: "Sign in again",
|
|
66
|
+
email: "Email",
|
|
67
|
+
password: "Password",
|
|
68
|
+
signInWithPasskey: "Sign in with a passkey",
|
|
69
|
+
passkeyFor: (domain) => `Use a passkey for ${domain}`,
|
|
70
|
+
emailMeACode: "Email me a code",
|
|
71
|
+
or: "or",
|
|
72
|
+
code: "Code",
|
|
73
|
+
codeSentTo: (email) => `We sent a code to ${email}.`,
|
|
74
|
+
verifyYourEmail: (email) => `Enter the code we sent to ${email} to verify your email address.`,
|
|
75
|
+
verify: "Verify",
|
|
76
|
+
sendAnotherCode: "Send another code",
|
|
77
|
+
useAnotherWay: "Use another way",
|
|
78
|
+
skipForNow: "Skip for now",
|
|
79
|
+
forgotPassword: "Forgot your password?",
|
|
80
|
+
createAccount: "Create account",
|
|
81
|
+
newHere: "New here?",
|
|
82
|
+
haveAnAccount: "Have an account?",
|
|
83
|
+
continuingToSignIn: "Taking you to sign in…",
|
|
84
|
+
tryAgainIn: (seconds) => seconds < 60 ? `Try again in ${seconds} seconds.` : `Try again in ${Math.ceil(seconds / 60)} minutes.`,
|
|
85
|
+
signInProblems: {
|
|
86
|
+
missing: "Fill in every field.",
|
|
87
|
+
invalid_email: "Enter a valid email address.",
|
|
88
|
+
wrong_credentials: "That email and password don’t match.",
|
|
89
|
+
email_taken: "An account already uses that email address. Sign in instead.",
|
|
90
|
+
password_too_short: "That password is too short.",
|
|
91
|
+
password_too_long: "That password is too long.",
|
|
92
|
+
password_guessable: "That password is too easy to guess.",
|
|
93
|
+
password_breached: "That password has appeared in a data breach. Choose another.",
|
|
94
|
+
wrong_code: "That code isn’t right.",
|
|
95
|
+
code_expired: "That code has expired. Send another.",
|
|
96
|
+
code_used_up: "Too many tries with that code. Send another.",
|
|
97
|
+
address_suppressed: "Email to that address doesn’t arrive. Use another address.",
|
|
98
|
+
team_only: "Only the team can sign in here.",
|
|
99
|
+
send_failed: "The email couldn’t be sent. Try again.",
|
|
100
|
+
passkey_failed: "That passkey didn’t work. Try again.",
|
|
101
|
+
passkey_unknown: "That passkey isn’t for an account here.",
|
|
102
|
+
passkey_expired: "That took too long. Try your passkey again.",
|
|
103
|
+
passkey_cancelled: "No passkey was used.",
|
|
104
|
+
slow_down: "Wait a moment, then try again.",
|
|
105
|
+
locked: "Too many tries.",
|
|
106
|
+
limited: "Too many tries.",
|
|
107
|
+
expired: "That sign-in timed out. Start again.",
|
|
108
|
+
unavailable: "Sign-in isn’t working right now. Try again."
|
|
109
|
+
},
|
|
110
|
+
signInRefusals: {
|
|
111
|
+
disabled: "This account is disabled.",
|
|
112
|
+
deleting: "This account is being deleted.",
|
|
113
|
+
sign_up_closed: "New accounts can’t be made here.",
|
|
114
|
+
already_exists: "An account already uses that email address."
|
|
115
|
+
}
|
|
43
116
|
};
|
|
44
117
|
//#endregion
|
|
45
118
|
//#region src/mount.ts
|
|
@@ -101,7 +174,9 @@ const ROUTES = {
|
|
|
101
174
|
},
|
|
102
175
|
wallets: list("account/v1/wallets"),
|
|
103
176
|
accounts: list("account/v1/identities"),
|
|
104
|
-
sessions: list("account/v1/sessions")
|
|
177
|
+
sessions: list("account/v1/sessions"),
|
|
178
|
+
apiKeys: list("account/v1/api-keys"),
|
|
179
|
+
keyableApis: list("account/v1/apis")
|
|
105
180
|
};
|
|
106
181
|
const EMPTY = {
|
|
107
182
|
data: null,
|
|
@@ -113,7 +188,9 @@ const NO_RESOURCES = {
|
|
|
113
188
|
metadata: EMPTY,
|
|
114
189
|
wallets: EMPTY,
|
|
115
190
|
accounts: EMPTY,
|
|
116
|
-
sessions: EMPTY
|
|
191
|
+
sessions: EMPTY,
|
|
192
|
+
apiKeys: EMPTY,
|
|
193
|
+
keyableApis: EMPTY
|
|
117
194
|
};
|
|
118
195
|
function createStore(request, initial) {
|
|
119
196
|
let state = {
|
|
@@ -400,6 +477,14 @@ function useLinkedAccounts() {
|
|
|
400
477
|
function useAccountSessions() {
|
|
401
478
|
return useAccountData("sessions");
|
|
402
479
|
}
|
|
480
|
+
/** The person's API keys for the brand's APIs (AERO-1693), revoked and expired ones too. */
|
|
481
|
+
function useApiKeys() {
|
|
482
|
+
return useAccountData("apiKeys");
|
|
483
|
+
}
|
|
484
|
+
/** The brand's APIs that let the person make keys, with the scopes each defines. */
|
|
485
|
+
function useKeyableApis() {
|
|
486
|
+
return useAccountData("keyableApis");
|
|
487
|
+
}
|
|
403
488
|
/** The signed-in person: their ID token's claims at once, then their profile and metadata. */
|
|
404
489
|
function useUser() {
|
|
405
490
|
const session = useSession();
|
|
@@ -439,7 +524,7 @@ function useAuth() {
|
|
|
439
524
|
const signingOut = useRef(null);
|
|
440
525
|
return useMemo(() => {
|
|
441
526
|
const signInUrl = (options = {}) => {
|
|
442
|
-
const params = new URLSearchParams({ returnTo: options.returnTo ?? currentPath() });
|
|
527
|
+
const params = new URLSearchParams({ returnTo: options.returnTo ?? currentPath$1() });
|
|
443
528
|
if (options.method) params.set("method", options.method);
|
|
444
529
|
if (options.loginHint) params.set("login_hint", options.loginHint);
|
|
445
530
|
if (options.prompt) params.set("prompt", options.prompt);
|
|
@@ -474,7 +559,7 @@ function useAuth() {
|
|
|
474
559
|
headers: { "content-type": "application/json" },
|
|
475
560
|
body: JSON.stringify({
|
|
476
561
|
method,
|
|
477
|
-
returnTo: options.returnTo ?? currentPath()
|
|
562
|
+
returnTo: options.returnTo ?? currentPath$1()
|
|
478
563
|
})
|
|
479
564
|
});
|
|
480
565
|
if (result.error) return result;
|
|
@@ -518,6 +603,29 @@ function useAuth() {
|
|
|
518
603
|
return result;
|
|
519
604
|
}
|
|
520
605
|
},
|
|
606
|
+
apiKeys: {
|
|
607
|
+
list: read("apiKeys"),
|
|
608
|
+
async create(input) {
|
|
609
|
+
const result = await change("POST", "v1/api-keys", input);
|
|
610
|
+
const { data } = result;
|
|
611
|
+
if (data) {
|
|
612
|
+
const made = withoutKey(data);
|
|
613
|
+
store.put("apiKeys", (list) => list && [...list, made]);
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
},
|
|
617
|
+
async revoke(revoked) {
|
|
618
|
+
const result = await change("DELETE", `v1/api-keys/${id(revoked)}`);
|
|
619
|
+
if (!result.error) {
|
|
620
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
621
|
+
store.put("apiKeys", (list) => list && list.map((k) => k.id === revoked ? {
|
|
622
|
+
...k,
|
|
623
|
+
revoked_at: at
|
|
624
|
+
} : k));
|
|
625
|
+
}
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
},
|
|
521
629
|
plugins
|
|
522
630
|
};
|
|
523
631
|
}, [
|
|
@@ -530,6 +638,12 @@ function useAuth() {
|
|
|
530
638
|
change
|
|
531
639
|
]);
|
|
532
640
|
}
|
|
641
|
+
/** A new key as the list keeps it: its description, never the key itself. */
|
|
642
|
+
function withoutKey(made) {
|
|
643
|
+
const copy = { ...made };
|
|
644
|
+
delete copy.key;
|
|
645
|
+
return copy;
|
|
646
|
+
}
|
|
533
647
|
/**
|
|
534
648
|
* Sends changes to the account API for one mounted component. Each change
|
|
535
649
|
* carries an idempotency key minted here; the key is used again only when
|
|
@@ -569,6 +683,278 @@ function useChanges() {
|
|
|
569
683
|
return sent;
|
|
570
684
|
}, [store, request]);
|
|
571
685
|
}
|
|
686
|
+
function currentPath$1() {
|
|
687
|
+
if (typeof window === "undefined") return "/";
|
|
688
|
+
return `${window.location.pathname}${window.location.search}`;
|
|
689
|
+
}
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/sign-in.ts
|
|
692
|
+
function webAuthn() {
|
|
693
|
+
return globalThis.PublicKeyCredential;
|
|
694
|
+
}
|
|
695
|
+
/** Whether this browser takes the issuer's passkeys from this page, and offers them as people type. */
|
|
696
|
+
async function passkeyCapabilities() {
|
|
697
|
+
const statics = webAuthn();
|
|
698
|
+
if (!statics?.getClientCapabilities || !statics.parseRequestOptionsFromJSON) return {
|
|
699
|
+
supported: false,
|
|
700
|
+
autofill: false
|
|
701
|
+
};
|
|
702
|
+
try {
|
|
703
|
+
const capabilities = await statics.getClientCapabilities();
|
|
704
|
+
const supported = capabilities.relatedOrigins === true;
|
|
705
|
+
return {
|
|
706
|
+
supported,
|
|
707
|
+
autofill: supported && capabilities.conditionalGet === true
|
|
708
|
+
};
|
|
709
|
+
} catch {
|
|
710
|
+
return {
|
|
711
|
+
supported: false,
|
|
712
|
+
autofill: false
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/** One sign-in on the app's own screens, through the route mount's `sign-in/flow`. */
|
|
717
|
+
function useSignIn(options = {}) {
|
|
718
|
+
const { request, navigate, store } = useSynqContext();
|
|
719
|
+
const auth = useAuth();
|
|
720
|
+
const [step, setStep] = useState("start");
|
|
721
|
+
const [pending, setPending] = useState(0);
|
|
722
|
+
const [problem, setProblem] = useState(null);
|
|
723
|
+
const [email, setEmail] = useState(void 0);
|
|
724
|
+
const [canSkip, setCanSkip] = useState(false);
|
|
725
|
+
const [handover, setHandover] = useState(null);
|
|
726
|
+
const [relatedDomain, setRelatedDomain] = useState(null);
|
|
727
|
+
const [capabilities, setCapabilities] = useState({
|
|
728
|
+
supported: false,
|
|
729
|
+
autofill: false
|
|
730
|
+
});
|
|
731
|
+
const autofilling = useRef(null);
|
|
732
|
+
const latest = useRef(options);
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
latest.current = options;
|
|
735
|
+
});
|
|
736
|
+
const { returnTo } = options;
|
|
737
|
+
useEffect(() => {
|
|
738
|
+
let live = true;
|
|
739
|
+
passkeyCapabilities().then((found) => {
|
|
740
|
+
if (live) setCapabilities(found);
|
|
741
|
+
});
|
|
742
|
+
return () => {
|
|
743
|
+
live = false;
|
|
744
|
+
autofilling.current?.abort();
|
|
745
|
+
};
|
|
746
|
+
}, []);
|
|
747
|
+
const actions = useMemo(() => {
|
|
748
|
+
async function post(body) {
|
|
749
|
+
setPending((n) => n + 1);
|
|
750
|
+
try {
|
|
751
|
+
const result = await call(request, "sign-in/flow", {
|
|
752
|
+
method: "POST",
|
|
753
|
+
headers: { "content-type": "application/json" },
|
|
754
|
+
body: JSON.stringify({
|
|
755
|
+
...body,
|
|
756
|
+
returnTo: latest.current.returnTo ?? currentPath()
|
|
757
|
+
})
|
|
758
|
+
});
|
|
759
|
+
if (result.error) {
|
|
760
|
+
setProblem({
|
|
761
|
+
code: "unavailable",
|
|
762
|
+
message: result.error.message
|
|
763
|
+
});
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
return result.data;
|
|
767
|
+
} finally {
|
|
768
|
+
setPending((n) => n - 1);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Shows what the mount answered. */
|
|
772
|
+
async function show(answer) {
|
|
773
|
+
if (!answer) return;
|
|
774
|
+
switch (answer.status) {
|
|
775
|
+
case "signed-in":
|
|
776
|
+
autofilling.current?.abort();
|
|
777
|
+
setStep("done");
|
|
778
|
+
setProblem(null);
|
|
779
|
+
await store.refreshSession();
|
|
780
|
+
if (latest.current.onSignedIn) latest.current.onSignedIn(answer.returnTo);
|
|
781
|
+
else navigate(answer.returnTo);
|
|
782
|
+
return;
|
|
783
|
+
case "code-sent":
|
|
784
|
+
setStep("code");
|
|
785
|
+
setEmail(answer.email);
|
|
786
|
+
setProblem(null);
|
|
787
|
+
return;
|
|
788
|
+
case "verify-email":
|
|
789
|
+
setStep("verify-email");
|
|
790
|
+
setEmail(answer.email);
|
|
791
|
+
setCanSkip(answer.canSkip);
|
|
792
|
+
setProblem(null);
|
|
793
|
+
return;
|
|
794
|
+
case "passkey":
|
|
795
|
+
setRelatedDomain(answer.passkey.related?.rpId ?? null);
|
|
796
|
+
if (answer.problem) setProblem({
|
|
797
|
+
code: answer.problem,
|
|
798
|
+
message: answer.message ?? ""
|
|
799
|
+
});
|
|
800
|
+
return;
|
|
801
|
+
case "retry":
|
|
802
|
+
setProblem({
|
|
803
|
+
code: answer.problem,
|
|
804
|
+
message: answer.message,
|
|
805
|
+
...answer.retryAfter === void 0 ? {} : { retryAfter: answer.retryAfter }
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
case "hosted":
|
|
809
|
+
autofilling.current?.abort();
|
|
810
|
+
setHandover(answer.hosted);
|
|
811
|
+
return;
|
|
812
|
+
case "refused":
|
|
813
|
+
setStep("start");
|
|
814
|
+
setProblem({
|
|
815
|
+
code: "refused",
|
|
816
|
+
reason: answer.reason,
|
|
817
|
+
message: answer.message
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
case "expired":
|
|
821
|
+
setStep("start");
|
|
822
|
+
setProblem({
|
|
823
|
+
code: "expired",
|
|
824
|
+
message: answer.message
|
|
825
|
+
});
|
|
826
|
+
return;
|
|
827
|
+
case "restarted": return;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const run = async (body) => show(await post(body));
|
|
831
|
+
/** Asks Synq for a passkey's options, the browser for the passkey, and Synq to take it. */
|
|
832
|
+
async function passkey(related, mediation) {
|
|
833
|
+
const statics = webAuthn();
|
|
834
|
+
if (!statics?.parseRequestOptionsFromJSON) return;
|
|
835
|
+
autofilling.current?.abort();
|
|
836
|
+
const answer = await post({ step: "passkey-options" });
|
|
837
|
+
if (answer?.status !== "passkey") {
|
|
838
|
+
if (answer?.status === "hosted" && answer.hosted.reason === "passkey_unavailable") setCapabilities({
|
|
839
|
+
supported: false,
|
|
840
|
+
autofill: false
|
|
841
|
+
});
|
|
842
|
+
if (mediation) return;
|
|
843
|
+
return show(answer);
|
|
844
|
+
}
|
|
845
|
+
setRelatedDomain(answer.passkey.related?.rpId ?? null);
|
|
846
|
+
const chosen = related ? answer.passkey.related : void 0;
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
if (mediation) autofilling.current = controller;
|
|
849
|
+
let credential;
|
|
850
|
+
try {
|
|
851
|
+
credential = await navigator.credentials.get({
|
|
852
|
+
publicKey: statics.parseRequestOptionsFromJSON(chosen?.options ?? answer.passkey.options),
|
|
853
|
+
signal: controller.signal,
|
|
854
|
+
...mediation ? { mediation } : {}
|
|
855
|
+
});
|
|
856
|
+
} catch (error) {
|
|
857
|
+
if (!mediation && !controller.signal.aborted) setProblem({
|
|
858
|
+
code: "passkey_cancelled",
|
|
859
|
+
message: String(error)
|
|
860
|
+
});
|
|
861
|
+
return;
|
|
862
|
+
} finally {
|
|
863
|
+
if (autofilling.current === controller) autofilling.current = null;
|
|
864
|
+
}
|
|
865
|
+
if (!credential) return;
|
|
866
|
+
await run({
|
|
867
|
+
step: "passkey",
|
|
868
|
+
credential: credential.toJSON(),
|
|
869
|
+
related: Boolean(chosen)
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
password: {
|
|
874
|
+
signIn: ({ email: address, password }) => run({
|
|
875
|
+
step: "password",
|
|
876
|
+
email: address,
|
|
877
|
+
password
|
|
878
|
+
}),
|
|
879
|
+
signUp: ({ email: address, password }) => run({
|
|
880
|
+
step: "password",
|
|
881
|
+
screen: "sign-up",
|
|
882
|
+
email: address,
|
|
883
|
+
password
|
|
884
|
+
})
|
|
885
|
+
},
|
|
886
|
+
emailCode: {
|
|
887
|
+
send: (address) => run({
|
|
888
|
+
step: "email",
|
|
889
|
+
email: address
|
|
890
|
+
}),
|
|
891
|
+
verify: (code) => run({
|
|
892
|
+
step: "code",
|
|
893
|
+
code
|
|
894
|
+
}),
|
|
895
|
+
resend: () => run({ step: "resend" })
|
|
896
|
+
},
|
|
897
|
+
verifyEmail: {
|
|
898
|
+
verify: (code) => run({
|
|
899
|
+
step: "code",
|
|
900
|
+
code
|
|
901
|
+
}),
|
|
902
|
+
resend: () => run({ step: "resend" }),
|
|
903
|
+
skip: () => run({ step: "skip" })
|
|
904
|
+
},
|
|
905
|
+
passkeySignIn: ({ related = false } = {}) => passkey(related),
|
|
906
|
+
passkeyAutofill: () => passkey(false, "conditional"),
|
|
907
|
+
startOver: async () => {
|
|
908
|
+
autofilling.current?.abort();
|
|
909
|
+
await post({ step: "restart" });
|
|
910
|
+
setStep("start");
|
|
911
|
+
setProblem(null);
|
|
912
|
+
setEmail(void 0);
|
|
913
|
+
setCanSkip(false);
|
|
914
|
+
setHandover(null);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}, [
|
|
918
|
+
request,
|
|
919
|
+
navigate,
|
|
920
|
+
store
|
|
921
|
+
]);
|
|
922
|
+
const startAutofill = useCallback(() => capabilities.autofill ? actions.passkeyAutofill() : Promise.resolve(), [capabilities.autofill, actions]);
|
|
923
|
+
const continueOnHostedPage = useCallback((overrides = {}) => {
|
|
924
|
+
const method = overrides.method ?? handover?.method;
|
|
925
|
+
const loginHint = overrides.loginHint ?? handover?.loginHint ?? email;
|
|
926
|
+
auth.signIn({
|
|
927
|
+
...returnTo ? { returnTo } : {},
|
|
928
|
+
...method ? { method } : {},
|
|
929
|
+
...loginHint ? { loginHint } : {}
|
|
930
|
+
});
|
|
931
|
+
}, [
|
|
932
|
+
auth,
|
|
933
|
+
handover,
|
|
934
|
+
email,
|
|
935
|
+
returnTo
|
|
936
|
+
]);
|
|
937
|
+
return {
|
|
938
|
+
step,
|
|
939
|
+
pending: pending > 0,
|
|
940
|
+
problem,
|
|
941
|
+
email,
|
|
942
|
+
canSkip,
|
|
943
|
+
handover,
|
|
944
|
+
password: actions.password,
|
|
945
|
+
emailCode: actions.emailCode,
|
|
946
|
+
verifyEmail: actions.verifyEmail,
|
|
947
|
+
passkey: {
|
|
948
|
+
supported: capabilities.supported,
|
|
949
|
+
autofill: capabilities.autofill,
|
|
950
|
+
relatedDomain,
|
|
951
|
+
signIn: actions.passkeySignIn,
|
|
952
|
+
startAutofill
|
|
953
|
+
},
|
|
954
|
+
continueOnHostedPage,
|
|
955
|
+
startOver: actions.startOver
|
|
956
|
+
};
|
|
957
|
+
}
|
|
572
958
|
function currentPath() {
|
|
573
959
|
if (typeof window === "undefined") return "/";
|
|
574
960
|
return `${window.location.pathname}${window.location.search}`;
|
|
@@ -620,6 +1006,422 @@ function SignInButton({ unstyled, className, children, ...options }) {
|
|
|
620
1006
|
});
|
|
621
1007
|
}
|
|
622
1008
|
//#endregion
|
|
1009
|
+
//#region src/components/SignInForm.tsx
|
|
1010
|
+
const ALL_METHODS = [
|
|
1011
|
+
"passkey",
|
|
1012
|
+
"email",
|
|
1013
|
+
"password"
|
|
1014
|
+
];
|
|
1015
|
+
/**
|
|
1016
|
+
* Sign-in on the app's own page: a passkey where the browser takes the
|
|
1017
|
+
* issuer's, then an emailed code or a password, and the code or the new
|
|
1018
|
+
* address's proof after. Whatever only the hosted page can do, it goes
|
|
1019
|
+
* there for.
|
|
1020
|
+
*/
|
|
1021
|
+
function SignInForm({ methods = ALL_METHODS, screen: initialScreen = "sign-in", returnTo, onSignedIn, unstyled, className }) {
|
|
1022
|
+
const signIn = useSignIn({
|
|
1023
|
+
...returnTo === void 0 ? {} : { returnTo },
|
|
1024
|
+
...onSignedIn === void 0 ? {} : { onSignedIn }
|
|
1025
|
+
});
|
|
1026
|
+
const m = useSynqMessages();
|
|
1027
|
+
const c = styler(unstyled);
|
|
1028
|
+
const [screen, setScreen] = useState(initialScreen);
|
|
1029
|
+
const passwords = methods.includes("password");
|
|
1030
|
+
const codes = methods.includes("email");
|
|
1031
|
+
const passkeys = methods.includes("passkey") && signIn.passkey.supported;
|
|
1032
|
+
const signingUp = screen === "sign-up" && passwords;
|
|
1033
|
+
const { step, handover, continueOnHostedPage } = signIn;
|
|
1034
|
+
useEffect(() => {
|
|
1035
|
+
if (handover) continueOnHostedPage();
|
|
1036
|
+
}, [handover, continueOnHostedPage]);
|
|
1037
|
+
const root = rootClass(unstyled, "sign-in-form", className);
|
|
1038
|
+
if (handover || step === "done") return /* @__PURE__ */ jsx("div", {
|
|
1039
|
+
className: root,
|
|
1040
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1041
|
+
className: c("muted"),
|
|
1042
|
+
role: "status",
|
|
1043
|
+
children: handover ? m.continuingToSignIn : m.signedIn
|
|
1044
|
+
})
|
|
1045
|
+
});
|
|
1046
|
+
if (step === "code") return /* @__PURE__ */ jsx("div", {
|
|
1047
|
+
className: root,
|
|
1048
|
+
children: /* @__PURE__ */ jsx(EmailCodeForm, {
|
|
1049
|
+
signIn,
|
|
1050
|
+
unstyled
|
|
1051
|
+
})
|
|
1052
|
+
});
|
|
1053
|
+
if (step === "verify-email") return /* @__PURE__ */ jsx("div", {
|
|
1054
|
+
className: root,
|
|
1055
|
+
children: /* @__PURE__ */ jsx(VerifyEmailForm, {
|
|
1056
|
+
signIn,
|
|
1057
|
+
unstyled
|
|
1058
|
+
})
|
|
1059
|
+
});
|
|
1060
|
+
const others = codes || passwords;
|
|
1061
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1062
|
+
className: root,
|
|
1063
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1064
|
+
className: c("form"),
|
|
1065
|
+
children: [
|
|
1066
|
+
passkeys && !signingUp ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1067
|
+
/* @__PURE__ */ jsx(PasskeyButton, {
|
|
1068
|
+
signIn,
|
|
1069
|
+
unstyled
|
|
1070
|
+
}),
|
|
1071
|
+
signIn.passkey.relatedDomain ? /* @__PURE__ */ jsx(PasskeyButton, {
|
|
1072
|
+
signIn,
|
|
1073
|
+
related: true,
|
|
1074
|
+
unstyled
|
|
1075
|
+
}) : null,
|
|
1076
|
+
others ? /* @__PURE__ */ jsx("p", {
|
|
1077
|
+
className: c("muted"),
|
|
1078
|
+
children: m.or
|
|
1079
|
+
}) : null
|
|
1080
|
+
] }) : null,
|
|
1081
|
+
passwords ? /* @__PURE__ */ jsx(PasswordForm, {
|
|
1082
|
+
signIn,
|
|
1083
|
+
screen: signingUp ? "sign-up" : "sign-in",
|
|
1084
|
+
emailCode: codes,
|
|
1085
|
+
autofill: passkeys,
|
|
1086
|
+
unstyled
|
|
1087
|
+
}) : codes ? /* @__PURE__ */ jsx(EmailCodeForm, {
|
|
1088
|
+
signIn,
|
|
1089
|
+
autofill: passkeys,
|
|
1090
|
+
unstyled
|
|
1091
|
+
}) : signIn.problem ? /* @__PURE__ */ jsx(ProblemText, {
|
|
1092
|
+
problem: signIn.problem,
|
|
1093
|
+
m,
|
|
1094
|
+
c
|
|
1095
|
+
}) : null,
|
|
1096
|
+
passwords ? /* @__PURE__ */ jsxs("p", {
|
|
1097
|
+
className: c("muted"),
|
|
1098
|
+
children: [
|
|
1099
|
+
signingUp ? m.haveAnAccount : m.newHere,
|
|
1100
|
+
" ",
|
|
1101
|
+
/* @__PURE__ */ jsx("button", {
|
|
1102
|
+
type: "button",
|
|
1103
|
+
className: c("link"),
|
|
1104
|
+
onClick: () => setScreen(signingUp ? "sign-in" : "sign-up"),
|
|
1105
|
+
children: signingUp ? m.signIn : m.createAccount
|
|
1106
|
+
})
|
|
1107
|
+
]
|
|
1108
|
+
}) : null
|
|
1109
|
+
]
|
|
1110
|
+
})
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
/** The button for a passkey: the brand's, or with `related` its own host's on its custom domain. */
|
|
1114
|
+
function PasskeyButton({ signIn, related = false, children, unstyled, className }) {
|
|
1115
|
+
const m = useSynqMessages();
|
|
1116
|
+
const domain = signIn.passkey.relatedDomain;
|
|
1117
|
+
if (!signIn.passkey.supported || related && !domain) return null;
|
|
1118
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1119
|
+
type: "button",
|
|
1120
|
+
className: rootClass(unstyled, "button", className),
|
|
1121
|
+
disabled: signIn.pending,
|
|
1122
|
+
onClick: () => void signIn.passkey.signIn({ related }),
|
|
1123
|
+
children: children ?? (related && domain ? m.passkeyFor(domain) : m.signInWithPasskey)
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* An email address and a password: signs in, or with `screen="sign-up"`
|
|
1128
|
+
* makes an account. With `emailCode`, it offers a code by email for the
|
|
1129
|
+
* same address too.
|
|
1130
|
+
*/
|
|
1131
|
+
function PasswordForm({ signIn, screen = "sign-in", emailCode = false, autofill = false, unstyled, className }) {
|
|
1132
|
+
const m = useSynqMessages();
|
|
1133
|
+
const c = styler(unstyled);
|
|
1134
|
+
const signingUp = screen === "sign-up";
|
|
1135
|
+
const onSubmit = (event) => {
|
|
1136
|
+
event.preventDefault();
|
|
1137
|
+
const data = new FormData(event.currentTarget);
|
|
1138
|
+
const input = {
|
|
1139
|
+
email: field(data, "email").trim(),
|
|
1140
|
+
password: field(data, "password")
|
|
1141
|
+
};
|
|
1142
|
+
signingUp ? signIn.password.signUp(input) : signIn.password.signIn(input);
|
|
1143
|
+
};
|
|
1144
|
+
return /* @__PURE__ */ jsx("form", {
|
|
1145
|
+
className: rootClass(unstyled, "password-form", className),
|
|
1146
|
+
onSubmit,
|
|
1147
|
+
"aria-busy": signIn.pending,
|
|
1148
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1149
|
+
className: c("form"),
|
|
1150
|
+
children: [
|
|
1151
|
+
/* @__PURE__ */ jsx(EmailField, {
|
|
1152
|
+
signIn,
|
|
1153
|
+
autofill: autofill && !signingUp,
|
|
1154
|
+
signingUp,
|
|
1155
|
+
c
|
|
1156
|
+
}),
|
|
1157
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1158
|
+
className: c("field"),
|
|
1159
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1160
|
+
className: c("label"),
|
|
1161
|
+
children: m.password
|
|
1162
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1163
|
+
className: c("input"),
|
|
1164
|
+
name: "password",
|
|
1165
|
+
type: "password",
|
|
1166
|
+
required: true,
|
|
1167
|
+
autoComplete: signingUp ? "new-password" : "current-password"
|
|
1168
|
+
})]
|
|
1169
|
+
}),
|
|
1170
|
+
signIn.problem ? /* @__PURE__ */ jsx(ProblemText, {
|
|
1171
|
+
problem: signIn.problem,
|
|
1172
|
+
m,
|
|
1173
|
+
c
|
|
1174
|
+
}) : null,
|
|
1175
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1176
|
+
className: c("actions"),
|
|
1177
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1178
|
+
type: "submit",
|
|
1179
|
+
className: c("button", "button-primary"),
|
|
1180
|
+
disabled: signIn.pending,
|
|
1181
|
+
children: signingUp ? m.createAccount : m.signIn
|
|
1182
|
+
}), emailCode && !signingUp ? /* @__PURE__ */ jsx("button", {
|
|
1183
|
+
type: "button",
|
|
1184
|
+
className: c("button"),
|
|
1185
|
+
disabled: signIn.pending,
|
|
1186
|
+
onClick: (event) => {
|
|
1187
|
+
const email = event.currentTarget.form?.elements.namedItem("email");
|
|
1188
|
+
if (!(email instanceof HTMLInputElement) || !email.reportValidity()) return;
|
|
1189
|
+
signIn.emailCode.send(email.value.trim());
|
|
1190
|
+
},
|
|
1191
|
+
children: m.emailMeACode
|
|
1192
|
+
}) : null]
|
|
1193
|
+
}),
|
|
1194
|
+
signingUp ? null : /* @__PURE__ */ jsx("button", {
|
|
1195
|
+
type: "button",
|
|
1196
|
+
className: c("link"),
|
|
1197
|
+
onClick: (event) => {
|
|
1198
|
+
const email = event.currentTarget.form?.elements.namedItem("email");
|
|
1199
|
+
const typed = email instanceof HTMLInputElement ? email.value.trim() : "";
|
|
1200
|
+
signIn.continueOnHostedPage({
|
|
1201
|
+
method: "password",
|
|
1202
|
+
...typed ? { loginHint: typed } : {}
|
|
1203
|
+
});
|
|
1204
|
+
},
|
|
1205
|
+
children: m.forgotPassword
|
|
1206
|
+
})
|
|
1207
|
+
]
|
|
1208
|
+
})
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
/** An emailed code: the address to send it to, then the code it held. */
|
|
1212
|
+
function EmailCodeForm({ signIn, autofill = false, unstyled, className }) {
|
|
1213
|
+
const m = useSynqMessages();
|
|
1214
|
+
const c = styler(unstyled);
|
|
1215
|
+
const id = useId();
|
|
1216
|
+
const root = rootClass(unstyled, "email-code-form", className);
|
|
1217
|
+
const problem = signIn.problem ? /* @__PURE__ */ jsx(ProblemText, {
|
|
1218
|
+
problem: signIn.problem,
|
|
1219
|
+
m,
|
|
1220
|
+
c
|
|
1221
|
+
}) : null;
|
|
1222
|
+
if (signIn.step === "code") {
|
|
1223
|
+
const onSubmit = (event) => {
|
|
1224
|
+
event.preventDefault();
|
|
1225
|
+
signIn.emailCode.verify(field(new FormData(event.currentTarget), "code").trim());
|
|
1226
|
+
};
|
|
1227
|
+
return /* @__PURE__ */ jsx("form", {
|
|
1228
|
+
className: root,
|
|
1229
|
+
onSubmit,
|
|
1230
|
+
"aria-busy": signIn.pending,
|
|
1231
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1232
|
+
className: c("form"),
|
|
1233
|
+
children: [
|
|
1234
|
+
/* @__PURE__ */ jsx("p", {
|
|
1235
|
+
className: c("muted"),
|
|
1236
|
+
id: `${id}-sent`,
|
|
1237
|
+
children: m.codeSentTo(signIn.email ?? "")
|
|
1238
|
+
}),
|
|
1239
|
+
/* @__PURE__ */ jsx(OneTimeCodeInput, {
|
|
1240
|
+
label: m.code,
|
|
1241
|
+
"aria-describedby": `${id}-sent`,
|
|
1242
|
+
unstyled
|
|
1243
|
+
}),
|
|
1244
|
+
problem,
|
|
1245
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1246
|
+
className: c("actions"),
|
|
1247
|
+
children: [
|
|
1248
|
+
/* @__PURE__ */ jsx("button", {
|
|
1249
|
+
type: "submit",
|
|
1250
|
+
className: c("button", "button-primary"),
|
|
1251
|
+
disabled: signIn.pending,
|
|
1252
|
+
children: m.verify
|
|
1253
|
+
}),
|
|
1254
|
+
/* @__PURE__ */ jsx("button", {
|
|
1255
|
+
type: "button",
|
|
1256
|
+
className: c("button"),
|
|
1257
|
+
disabled: signIn.pending,
|
|
1258
|
+
onClick: () => void signIn.emailCode.resend(),
|
|
1259
|
+
children: m.sendAnotherCode
|
|
1260
|
+
}),
|
|
1261
|
+
/* @__PURE__ */ jsx("button", {
|
|
1262
|
+
type: "button",
|
|
1263
|
+
className: c("button"),
|
|
1264
|
+
disabled: signIn.pending,
|
|
1265
|
+
onClick: () => void signIn.startOver(),
|
|
1266
|
+
children: m.useAnotherWay
|
|
1267
|
+
})
|
|
1268
|
+
]
|
|
1269
|
+
})
|
|
1270
|
+
]
|
|
1271
|
+
})
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
const onSubmit = (event) => {
|
|
1275
|
+
event.preventDefault();
|
|
1276
|
+
signIn.emailCode.send(field(new FormData(event.currentTarget), "email").trim());
|
|
1277
|
+
};
|
|
1278
|
+
return /* @__PURE__ */ jsx("form", {
|
|
1279
|
+
className: root,
|
|
1280
|
+
onSubmit,
|
|
1281
|
+
"aria-busy": signIn.pending,
|
|
1282
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1283
|
+
className: c("form"),
|
|
1284
|
+
children: [
|
|
1285
|
+
/* @__PURE__ */ jsx(EmailField, {
|
|
1286
|
+
signIn,
|
|
1287
|
+
autofill,
|
|
1288
|
+
signingUp: false,
|
|
1289
|
+
c
|
|
1290
|
+
}),
|
|
1291
|
+
problem,
|
|
1292
|
+
/* @__PURE__ */ jsx("div", {
|
|
1293
|
+
className: c("actions"),
|
|
1294
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1295
|
+
type: "submit",
|
|
1296
|
+
className: c("button", "button-primary"),
|
|
1297
|
+
disabled: signIn.pending,
|
|
1298
|
+
children: m.emailMeACode
|
|
1299
|
+
})
|
|
1300
|
+
})
|
|
1301
|
+
]
|
|
1302
|
+
})
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
/** The code that proves a new account's address, or "Skip for now" where the brand allows. */
|
|
1306
|
+
function VerifyEmailForm({ signIn, unstyled, className }) {
|
|
1307
|
+
const m = useSynqMessages();
|
|
1308
|
+
const c = styler(unstyled);
|
|
1309
|
+
const id = useId();
|
|
1310
|
+
const onSubmit = (event) => {
|
|
1311
|
+
event.preventDefault();
|
|
1312
|
+
signIn.verifyEmail.verify(field(new FormData(event.currentTarget), "code").trim());
|
|
1313
|
+
};
|
|
1314
|
+
return /* @__PURE__ */ jsx("form", {
|
|
1315
|
+
className: rootClass(unstyled, "verify-email-form", className),
|
|
1316
|
+
onSubmit,
|
|
1317
|
+
"aria-busy": signIn.pending,
|
|
1318
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1319
|
+
className: c("form"),
|
|
1320
|
+
children: [
|
|
1321
|
+
/* @__PURE__ */ jsx("p", {
|
|
1322
|
+
className: c("muted"),
|
|
1323
|
+
id: `${id}-sent`,
|
|
1324
|
+
children: m.verifyYourEmail(signIn.email ?? "")
|
|
1325
|
+
}),
|
|
1326
|
+
/* @__PURE__ */ jsx(OneTimeCodeInput, {
|
|
1327
|
+
label: m.code,
|
|
1328
|
+
"aria-describedby": `${id}-sent`,
|
|
1329
|
+
unstyled
|
|
1330
|
+
}),
|
|
1331
|
+
signIn.problem ? /* @__PURE__ */ jsx(ProblemText, {
|
|
1332
|
+
problem: signIn.problem,
|
|
1333
|
+
m,
|
|
1334
|
+
c
|
|
1335
|
+
}) : null,
|
|
1336
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1337
|
+
className: c("actions"),
|
|
1338
|
+
children: [
|
|
1339
|
+
/* @__PURE__ */ jsx("button", {
|
|
1340
|
+
type: "submit",
|
|
1341
|
+
className: c("button", "button-primary"),
|
|
1342
|
+
disabled: signIn.pending,
|
|
1343
|
+
children: m.verify
|
|
1344
|
+
}),
|
|
1345
|
+
/* @__PURE__ */ jsx("button", {
|
|
1346
|
+
type: "button",
|
|
1347
|
+
className: c("button"),
|
|
1348
|
+
disabled: signIn.pending,
|
|
1349
|
+
onClick: () => void signIn.verifyEmail.resend(),
|
|
1350
|
+
children: m.sendAnotherCode
|
|
1351
|
+
}),
|
|
1352
|
+
signIn.canSkip ? /* @__PURE__ */ jsx("button", {
|
|
1353
|
+
type: "button",
|
|
1354
|
+
className: c("button"),
|
|
1355
|
+
disabled: signIn.pending,
|
|
1356
|
+
onClick: () => void signIn.verifyEmail.skip(),
|
|
1357
|
+
children: m.skipForNow
|
|
1358
|
+
}) : null
|
|
1359
|
+
]
|
|
1360
|
+
})
|
|
1361
|
+
]
|
|
1362
|
+
})
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
/** The field for an emailed code, named `code`: digits, filled from the message where the device can. */
|
|
1366
|
+
function OneTimeCodeInput({ label, unstyled, className, ...input }) {
|
|
1367
|
+
const c = styler(unstyled);
|
|
1368
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
1369
|
+
className: c("field"),
|
|
1370
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1371
|
+
className: c("label"),
|
|
1372
|
+
children: label
|
|
1373
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1374
|
+
className: [c("input", "code-input"), className].filter(Boolean).join(" ") || void 0,
|
|
1375
|
+
name: "code",
|
|
1376
|
+
required: true,
|
|
1377
|
+
inputMode: "numeric",
|
|
1378
|
+
pattern: "[0-9]*",
|
|
1379
|
+
maxLength: 10,
|
|
1380
|
+
autoComplete: "one-time-code",
|
|
1381
|
+
autoFocus: true,
|
|
1382
|
+
...input
|
|
1383
|
+
})]
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
/** The address field, which offers passkeys as the person types where it may, and starts that. */
|
|
1387
|
+
function EmailField({ signIn, autofill, signingUp, c }) {
|
|
1388
|
+
const m = useSynqMessages();
|
|
1389
|
+
const offer = autofill && signIn.passkey.autofill && signIn.step === "start";
|
|
1390
|
+
const startAutofill = signIn.passkey.startAutofill;
|
|
1391
|
+
useEffect(() => {
|
|
1392
|
+
if (offer) startAutofill();
|
|
1393
|
+
}, [offer, startAutofill]);
|
|
1394
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
1395
|
+
className: c("field"),
|
|
1396
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1397
|
+
className: c("label"),
|
|
1398
|
+
children: m.email
|
|
1399
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1400
|
+
className: c("input"),
|
|
1401
|
+
name: "email",
|
|
1402
|
+
type: "email",
|
|
1403
|
+
required: true,
|
|
1404
|
+
autoComplete: signingUp ? "email" : offer ? "username webauthn" : "username",
|
|
1405
|
+
defaultValue: signIn.email ?? ""
|
|
1406
|
+
})]
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/** What the person typed in a form's field. */
|
|
1410
|
+
function field(data, name) {
|
|
1411
|
+
const value = data.get(name);
|
|
1412
|
+
return typeof value === "string" ? value : "";
|
|
1413
|
+
}
|
|
1414
|
+
/** What went wrong, in the app's words where it gave them, and how long to wait. */
|
|
1415
|
+
function ProblemText({ problem, m, c }) {
|
|
1416
|
+
const said = (problem.code === "refused" && problem.reason ? m.signInRefusals[problem.reason] : void 0) ?? m.signInProblems[problem.code] ?? problem.message;
|
|
1417
|
+
const wait = problem.retryAfter === void 0 ? "" : ` ${m.tryAgainIn(problem.retryAfter)}`;
|
|
1418
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
1419
|
+
className: c("error"),
|
|
1420
|
+
role: "alert",
|
|
1421
|
+
children: [said, wait]
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
//#endregion
|
|
623
1425
|
//#region src/components/UserButton.tsx
|
|
624
1426
|
/**
|
|
625
1427
|
* The signed-in person's picture and name, showing a panel to manage their
|
|
@@ -717,7 +1519,8 @@ function initials(name) {
|
|
|
717
1519
|
const ALL = [
|
|
718
1520
|
"profile",
|
|
719
1521
|
"accounts",
|
|
720
|
-
"sessions"
|
|
1522
|
+
"sessions",
|
|
1523
|
+
"apiKeys"
|
|
721
1524
|
];
|
|
722
1525
|
/** Manage the signed-in person's account. Nothing while nobody is signed in. */
|
|
723
1526
|
function AccountManager({ unstyled, className, sections = ALL }) {
|
|
@@ -725,7 +1528,7 @@ function AccountManager({ unstyled, className, sections = ALL }) {
|
|
|
725
1528
|
if (!isSignedIn) return null;
|
|
726
1529
|
return /* @__PURE__ */ jsx("div", {
|
|
727
1530
|
className: rootClass(unstyled, "account-manager", className),
|
|
728
|
-
children: sections.map((section) => section === "profile" ? /* @__PURE__ */ jsx(ProfileSection, { unstyled }, section) : section === "accounts" ? /* @__PURE__ */ jsx(AccountsSection, { unstyled }, section) : /* @__PURE__ */ jsx(SessionsSection, { unstyled }, section))
|
|
1531
|
+
children: sections.map((section) => section === "profile" ? /* @__PURE__ */ jsx(ProfileSection, { unstyled }, section) : section === "accounts" ? /* @__PURE__ */ jsx(AccountsSection, { unstyled }, section) : section === "sessions" ? /* @__PURE__ */ jsx(SessionsSection, { unstyled }, section) : /* @__PURE__ */ jsx(ApiKeysSection, { unstyled }, section))
|
|
729
1532
|
});
|
|
730
1533
|
}
|
|
731
1534
|
function Section({ title, unstyled, children }) {
|
|
@@ -874,7 +1677,7 @@ function AccountRow({ account, removable, unstyled }) {
|
|
|
874
1677
|
const [failure, setFailure] = useState(null);
|
|
875
1678
|
const c = styler(unstyled);
|
|
876
1679
|
const provider = m.providers[account.provider] ?? capitalized(account.provider);
|
|
877
|
-
const subject = account.provider === "solana" ? shortAddress(account.subject) : account.subject;
|
|
1680
|
+
const subject = account.handle ? `@${account.handle}` : account.provider === "solana" ? shortAddress(account.subject) : account.subject;
|
|
878
1681
|
async function remove() {
|
|
879
1682
|
setBusy(true);
|
|
880
1683
|
const result = await accounts.unlink(account.id);
|
|
@@ -962,6 +1765,276 @@ function SessionsSection({ unstyled }) {
|
|
|
962
1765
|
})]
|
|
963
1766
|
});
|
|
964
1767
|
}
|
|
1768
|
+
/** How long a new key may last, in days: the account API takes up to 366. */
|
|
1769
|
+
const KEY_LIFETIMES = [
|
|
1770
|
+
30,
|
|
1771
|
+
90,
|
|
1772
|
+
365
|
|
1773
|
+
];
|
|
1774
|
+
function ApiKeysSection({ unstyled }) {
|
|
1775
|
+
const keys = useApiKeys();
|
|
1776
|
+
const apis = useKeyableApis();
|
|
1777
|
+
const m = useSynqMessages();
|
|
1778
|
+
const [made, setMade] = useState(null);
|
|
1779
|
+
const c = styler(unstyled);
|
|
1780
|
+
if ((apis.data?.length ?? 0) === 0 && (keys.data?.length ?? 0) === 0) return null;
|
|
1781
|
+
return /* @__PURE__ */ jsxs(Section, {
|
|
1782
|
+
title: m.apiKeys,
|
|
1783
|
+
unstyled,
|
|
1784
|
+
children: [
|
|
1785
|
+
made && /* @__PURE__ */ jsx(NewKey, {
|
|
1786
|
+
made,
|
|
1787
|
+
onDone: () => setMade(null),
|
|
1788
|
+
unstyled
|
|
1789
|
+
}),
|
|
1790
|
+
keys.data ? keys.data.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
1791
|
+
className: c("list"),
|
|
1792
|
+
children: keys.data.map((key) => /* @__PURE__ */ jsx(ApiKeyRow, {
|
|
1793
|
+
apiKey: key,
|
|
1794
|
+
unstyled
|
|
1795
|
+
}, key.id))
|
|
1796
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
1797
|
+
className: c("muted"),
|
|
1798
|
+
children: m.noApiKeys
|
|
1799
|
+
}) : /* @__PURE__ */ jsx(Pending, {
|
|
1800
|
+
isLoading: keys.isLoading,
|
|
1801
|
+
error: keys.error,
|
|
1802
|
+
unstyled
|
|
1803
|
+
}),
|
|
1804
|
+
apis.data && apis.data.length > 0 && /* @__PURE__ */ jsx(NewKeyForm, {
|
|
1805
|
+
apis: apis.data,
|
|
1806
|
+
onMade: setMade,
|
|
1807
|
+
unstyled
|
|
1808
|
+
})
|
|
1809
|
+
]
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
/** The key just made, shown this once, with a way to copy it. */
|
|
1813
|
+
function NewKey({ made, onDone, unstyled }) {
|
|
1814
|
+
const m = useSynqMessages();
|
|
1815
|
+
const [copied, setCopied] = useState(false);
|
|
1816
|
+
const c = styler(unstyled);
|
|
1817
|
+
async function copy() {
|
|
1818
|
+
try {
|
|
1819
|
+
await navigator.clipboard.writeText(made.key);
|
|
1820
|
+
setCopied(true);
|
|
1821
|
+
} catch {
|
|
1822
|
+
setCopied(false);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1826
|
+
role: "status",
|
|
1827
|
+
className: c("form"),
|
|
1828
|
+
children: [
|
|
1829
|
+
/* @__PURE__ */ jsx("p", {
|
|
1830
|
+
className: c("row-title"),
|
|
1831
|
+
children: m.copyKeyNow
|
|
1832
|
+
}),
|
|
1833
|
+
/* @__PURE__ */ jsx("code", {
|
|
1834
|
+
className: c("input"),
|
|
1835
|
+
"aria-label": made.name,
|
|
1836
|
+
children: made.key
|
|
1837
|
+
}),
|
|
1838
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1839
|
+
className: c("actions"),
|
|
1840
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1841
|
+
type: "button",
|
|
1842
|
+
className: c("button", "button-primary"),
|
|
1843
|
+
onClick: () => void copy(),
|
|
1844
|
+
children: copied ? m.copied : m.copy
|
|
1845
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1846
|
+
type: "button",
|
|
1847
|
+
className: c("button"),
|
|
1848
|
+
onClick: onDone,
|
|
1849
|
+
children: m.done
|
|
1850
|
+
})]
|
|
1851
|
+
})
|
|
1852
|
+
]
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
function ApiKeyRow({ apiKey, unstyled }) {
|
|
1856
|
+
const { apiKeys } = useAuth();
|
|
1857
|
+
const m = useSynqMessages();
|
|
1858
|
+
const [busy, setBusy] = useState(false);
|
|
1859
|
+
const [failure, setFailure] = useState(null);
|
|
1860
|
+
const [shownAt] = useState(() => Date.now());
|
|
1861
|
+
const c = styler(unstyled);
|
|
1862
|
+
const expired = apiKey.expires_at !== null && Date.parse(apiKey.expires_at) <= shownAt;
|
|
1863
|
+
const live = apiKey.revoked_at === null && !expired;
|
|
1864
|
+
async function revoke() {
|
|
1865
|
+
setBusy(true);
|
|
1866
|
+
const result = await apiKeys.revoke(apiKey.id);
|
|
1867
|
+
setBusy(false);
|
|
1868
|
+
setFailure(result.error ? failureText(result.error) : null);
|
|
1869
|
+
}
|
|
1870
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
1871
|
+
className: c("row"),
|
|
1872
|
+
children: [
|
|
1873
|
+
/* @__PURE__ */ jsx("span", {
|
|
1874
|
+
className: c("row-title"),
|
|
1875
|
+
children: apiKey.name
|
|
1876
|
+
}),
|
|
1877
|
+
apiKey.revoked_at !== null ? /* @__PURE__ */ jsx("span", {
|
|
1878
|
+
className: c("badge"),
|
|
1879
|
+
children: m.revoked
|
|
1880
|
+
}) : expired && /* @__PURE__ */ jsx("span", {
|
|
1881
|
+
className: c("badge"),
|
|
1882
|
+
children: m.expired
|
|
1883
|
+
}),
|
|
1884
|
+
/* @__PURE__ */ jsx("span", {
|
|
1885
|
+
className: c("muted"),
|
|
1886
|
+
children: `${apiKey.prefix}… · ${apiKey.api.name} · ${apiKey.scopes.join(", ")}`
|
|
1887
|
+
}),
|
|
1888
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1889
|
+
className: c("muted"),
|
|
1890
|
+
children: [apiKey.last_used_at ? m.lastUsed(formatDate(apiKey.last_used_at)) : m.neverUsed, apiKey.expires_at && live ? ` · ${m.expiresOn(formatDate(apiKey.expires_at))}` : ""]
|
|
1891
|
+
}),
|
|
1892
|
+
live && /* @__PURE__ */ jsx("button", {
|
|
1893
|
+
type: "button",
|
|
1894
|
+
className: c("button"),
|
|
1895
|
+
disabled: busy,
|
|
1896
|
+
"aria-label": m.revokeKey(apiKey.name),
|
|
1897
|
+
onClick: () => void revoke(),
|
|
1898
|
+
children: m.revoke
|
|
1899
|
+
}),
|
|
1900
|
+
failure && /* @__PURE__ */ jsx("p", {
|
|
1901
|
+
role: "alert",
|
|
1902
|
+
className: c("error"),
|
|
1903
|
+
children: failure
|
|
1904
|
+
})
|
|
1905
|
+
]
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
function NewKeyForm({ apis, onMade, unstyled }) {
|
|
1909
|
+
const { apiKeys, signIn } = useAuth();
|
|
1910
|
+
const m = useSynqMessages();
|
|
1911
|
+
const [api, setApi] = useState(apis[0].identifier);
|
|
1912
|
+
const [name, setName] = useState("");
|
|
1913
|
+
const [scopes, setScopes] = useState([]);
|
|
1914
|
+
const [lifetime, setLifetime] = useState(null);
|
|
1915
|
+
const [busy, setBusy] = useState(false);
|
|
1916
|
+
const [failure, setFailure] = useState(null);
|
|
1917
|
+
const scopeLegend = useId();
|
|
1918
|
+
const c = styler(unstyled);
|
|
1919
|
+
const chosen = apis.find((a) => a.identifier === api) ?? apis[0];
|
|
1920
|
+
async function make(event) {
|
|
1921
|
+
event.preventDefault();
|
|
1922
|
+
setBusy(true);
|
|
1923
|
+
setFailure(null);
|
|
1924
|
+
const result = await apiKeys.create({
|
|
1925
|
+
api: chosen.identifier,
|
|
1926
|
+
name: name.trim(),
|
|
1927
|
+
scopes: [...scopes],
|
|
1928
|
+
...lifetime === null ? {} : { expires_at: new Date(Date.now() + lifetime * 864e5).toISOString() }
|
|
1929
|
+
});
|
|
1930
|
+
setBusy(false);
|
|
1931
|
+
if (result.error) {
|
|
1932
|
+
setFailure({
|
|
1933
|
+
text: failureText(result.error),
|
|
1934
|
+
fresh: result.error.code === "fresh_sign_in_required"
|
|
1935
|
+
});
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
setName("");
|
|
1939
|
+
setScopes([]);
|
|
1940
|
+
onMade(result.data);
|
|
1941
|
+
}
|
|
1942
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
1943
|
+
className: c("form"),
|
|
1944
|
+
onSubmit: (event) => void make(event),
|
|
1945
|
+
children: [
|
|
1946
|
+
/* @__PURE__ */ jsx("h3", {
|
|
1947
|
+
className: c("row-title"),
|
|
1948
|
+
children: m.newApiKey
|
|
1949
|
+
}),
|
|
1950
|
+
apis.length > 1 && /* @__PURE__ */ jsxs("label", {
|
|
1951
|
+
className: c("field"),
|
|
1952
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1953
|
+
className: c("label"),
|
|
1954
|
+
children: m.api
|
|
1955
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
1956
|
+
className: c("input"),
|
|
1957
|
+
value: chosen.identifier,
|
|
1958
|
+
onChange: (event) => {
|
|
1959
|
+
setApi(event.target.value);
|
|
1960
|
+
setScopes([]);
|
|
1961
|
+
},
|
|
1962
|
+
children: apis.map((a) => /* @__PURE__ */ jsx("option", {
|
|
1963
|
+
value: a.identifier,
|
|
1964
|
+
children: a.name
|
|
1965
|
+
}, a.identifier))
|
|
1966
|
+
})]
|
|
1967
|
+
}),
|
|
1968
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1969
|
+
className: c("field"),
|
|
1970
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1971
|
+
className: c("label"),
|
|
1972
|
+
children: m.name
|
|
1973
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1974
|
+
className: c("input"),
|
|
1975
|
+
name: "key-name",
|
|
1976
|
+
required: true,
|
|
1977
|
+
maxLength: 100,
|
|
1978
|
+
value: name,
|
|
1979
|
+
onChange: (event) => setName(event.target.value)
|
|
1980
|
+
})]
|
|
1981
|
+
}),
|
|
1982
|
+
/* @__PURE__ */ jsxs("fieldset", {
|
|
1983
|
+
className: c("field"),
|
|
1984
|
+
"aria-labelledby": scopeLegend,
|
|
1985
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
1986
|
+
id: scopeLegend,
|
|
1987
|
+
className: c("label"),
|
|
1988
|
+
children: m.keyScopes
|
|
1989
|
+
}), chosen.scopes.map((scope) => /* @__PURE__ */ jsxs("label", {
|
|
1990
|
+
className: c("row"),
|
|
1991
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1992
|
+
type: "checkbox",
|
|
1993
|
+
checked: scopes.includes(scope.name),
|
|
1994
|
+
onChange: (event) => setScopes((now) => event.target.checked ? [...now, scope.name] : now.filter((one) => one !== scope.name))
|
|
1995
|
+
}), /* @__PURE__ */ jsx("span", { children: scope.description || scope.name })]
|
|
1996
|
+
}, scope.name))]
|
|
1997
|
+
}),
|
|
1998
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1999
|
+
className: c("field"),
|
|
2000
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2001
|
+
className: c("label"),
|
|
2002
|
+
children: m.expires
|
|
2003
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
2004
|
+
className: c("input"),
|
|
2005
|
+
value: lifetime === null ? "" : String(lifetime),
|
|
2006
|
+
onChange: (event) => setLifetime(event.target.value === "" ? null : Number(event.target.value)),
|
|
2007
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
2008
|
+
value: "",
|
|
2009
|
+
children: m.never
|
|
2010
|
+
}), KEY_LIFETIMES.map((days) => /* @__PURE__ */ jsx("option", {
|
|
2011
|
+
value: days,
|
|
2012
|
+
children: m.inDays(days)
|
|
2013
|
+
}, days))]
|
|
2014
|
+
})]
|
|
2015
|
+
}),
|
|
2016
|
+
/* @__PURE__ */ jsx("div", {
|
|
2017
|
+
className: c("actions"),
|
|
2018
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
2019
|
+
type: "submit",
|
|
2020
|
+
className: c("button", "button-primary"),
|
|
2021
|
+
disabled: busy || name.trim() === "" || scopes.length === 0,
|
|
2022
|
+
children: busy ? m.makingKey : m.makeKey
|
|
2023
|
+
})
|
|
2024
|
+
}),
|
|
2025
|
+
failure && /* @__PURE__ */ jsxs("div", {
|
|
2026
|
+
role: "alert",
|
|
2027
|
+
className: c("error"),
|
|
2028
|
+
children: [/* @__PURE__ */ jsx("p", { children: failure.fresh ? m.keyNeedsFreshSignIn : failure.text }), failure.fresh && /* @__PURE__ */ jsx("button", {
|
|
2029
|
+
type: "button",
|
|
2030
|
+
className: c("button"),
|
|
2031
|
+
onClick: () => signIn({ maxAge: 300 }),
|
|
2032
|
+
children: m.signInAgain
|
|
2033
|
+
})]
|
|
2034
|
+
})
|
|
2035
|
+
]
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
965
2038
|
function capitalized(provider) {
|
|
966
2039
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
967
2040
|
}
|
|
@@ -1104,4 +2177,4 @@ function WalletRow({ wallet, unstyled }) {
|
|
|
1104
2177
|
});
|
|
1105
2178
|
}
|
|
1106
2179
|
//#endregion
|
|
1107
|
-
export { AccountManager, DEFAULT_MESSAGES, SignInButton, SignedIn, SignedOut, SynqProvider, UserButton, WalletManager, useAccountSessions, useAuth, useLinkedAccounts, usePlugin, useSession, useUser, useWallets };
|
|
2180
|
+
export { AccountManager, DEFAULT_MESSAGES, EmailCodeForm, OneTimeCodeInput, PasskeyButton, PasswordForm, SignInButton, SignInForm, SignedIn, SignedOut, SynqProvider, UserButton, VerifyEmailForm, WalletManager, useAccountSessions, useApiKeys, useAuth, useKeyableApis, useLinkedAccounts, usePlugin, useSession, useSignIn, useUser, useWallets };
|