@swype-org/react-sdk 0.1.48 → 0.1.50

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/dist/index.cjs CHANGED
@@ -658,6 +658,12 @@ function normalizeSignature(sig) {
658
658
  }
659
659
 
660
660
  // src/passkey-delegation.ts
661
+ var PasskeyIframeBlockedError = class extends Error {
662
+ constructor(message = "Passkey creation is not supported in this browser context.") {
663
+ super(message);
664
+ this.name = "PasskeyIframeBlockedError";
665
+ }
666
+ };
661
667
  function isInCrossOriginIframe() {
662
668
  if (typeof window === "undefined") return false;
663
669
  if (window.parent === window) return false;
@@ -669,44 +675,73 @@ function isInCrossOriginIframe() {
669
675
  }
670
676
  }
671
677
  var delegationCounter = 0;
672
- function delegatePasskeyCreate(options) {
678
+ var DELEGATION_GET_TIMEOUT_MS = 3e4;
679
+ function delegatePasskeyGet(options) {
673
680
  return new Promise((resolve, reject) => {
674
- const id = `pc-${++delegationCounter}-${Date.now()}`;
681
+ const id = `pg-${++delegationCounter}-${Date.now()}`;
682
+ const timer = setTimeout(() => {
683
+ window.removeEventListener("message", handler);
684
+ reject(new Error("Passkey verification timed out. Please try again."));
685
+ }, DELEGATION_GET_TIMEOUT_MS);
675
686
  const handler = (event) => {
676
687
  const data = event.data;
677
688
  if (!data || typeof data !== "object") return;
678
- if (data.type !== "swype:passkey-create-response" || data.id !== id) return;
689
+ if (data.type !== "swype:passkey-get-response" || data.id !== id) return;
690
+ clearTimeout(timer);
679
691
  window.removeEventListener("message", handler);
680
692
  if (data.error) {
681
693
  reject(new Error(data.error));
682
694
  } else if (data.result) {
683
695
  resolve(data.result);
684
696
  } else {
685
- reject(new Error("Invalid passkey create response."));
697
+ reject(new Error("Invalid passkey get response."));
686
698
  }
687
699
  };
688
700
  window.addEventListener("message", handler);
689
- window.parent.postMessage({ type: "swype:passkey-create-request", id, options }, "*");
701
+ window.parent.postMessage({ type: "swype:passkey-get-request", id, options }, "*");
690
702
  });
691
703
  }
692
- function delegatePasskeyGet(options) {
704
+ var POPUP_RESULT_TIMEOUT_MS = 12e4;
705
+ var POPUP_CLOSED_POLL_MS = 500;
706
+ function createPasskeyViaPopup(options) {
693
707
  return new Promise((resolve, reject) => {
694
- const id = `pg-${++delegationCounter}-${Date.now()}`;
708
+ const encoded = btoa(JSON.stringify(options));
709
+ const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
710
+ const popup = window.open(popupUrl, "swype-passkey", "width=460,height=600");
711
+ if (!popup) {
712
+ reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
713
+ return;
714
+ }
715
+ const timer = setTimeout(() => {
716
+ cleanup();
717
+ reject(new Error("Passkey creation timed out. Please try again."));
718
+ }, POPUP_RESULT_TIMEOUT_MS);
719
+ const closedPoll = setInterval(() => {
720
+ if (popup.closed) {
721
+ cleanup();
722
+ reject(new Error("Passkey setup window was closed before completing."));
723
+ }
724
+ }, POPUP_CLOSED_POLL_MS);
695
725
  const handler = (event) => {
726
+ if (event.source !== popup) return;
696
727
  const data = event.data;
697
728
  if (!data || typeof data !== "object") return;
698
- if (data.type !== "swype:passkey-get-response" || data.id !== id) return;
699
- window.removeEventListener("message", handler);
729
+ if (data.type !== "swype:passkey-popup-result") return;
730
+ cleanup();
700
731
  if (data.error) {
701
732
  reject(new Error(data.error));
702
733
  } else if (data.result) {
703
734
  resolve(data.result);
704
735
  } else {
705
- reject(new Error("Invalid passkey get response."));
736
+ reject(new Error("Invalid passkey popup response."));
706
737
  }
707
738
  };
739
+ function cleanup() {
740
+ clearTimeout(timer);
741
+ clearInterval(closedPoll);
742
+ window.removeEventListener("message", handler);
743
+ }
708
744
  window.addEventListener("message", handler);
709
- window.parent.postMessage({ type: "swype:passkey-get-request", id, options }, "*");
710
745
  });
711
746
  }
712
747
 
@@ -831,53 +866,51 @@ async function createPasskeyCredential(params) {
831
866
  const challenge = new Uint8Array(32);
832
867
  crypto.getRandomValues(challenge);
833
868
  const rpId = resolvePasskeyRpId();
869
+ const publicKeyOptions = {
870
+ challenge,
871
+ rp: { name: "Swype", id: rpId },
872
+ user: {
873
+ id: new TextEncoder().encode(params.userId),
874
+ name: params.displayName,
875
+ displayName: params.displayName
876
+ },
877
+ pubKeyCredParams: [
878
+ { alg: -7, type: "public-key" },
879
+ { alg: -257, type: "public-key" }
880
+ ],
881
+ authenticatorSelection: {
882
+ authenticatorAttachment: "platform",
883
+ residentKey: "preferred",
884
+ userVerification: "required"
885
+ },
886
+ timeout: 6e4
887
+ };
834
888
  if (isInCrossOriginIframe()) {
835
- return delegatePasskeyCreate({
836
- challenge: toBase64(challenge),
837
- rpId,
838
- rpName: "Swype",
839
- userId: toBase64(new TextEncoder().encode(params.userId)),
840
- userName: params.displayName,
841
- userDisplayName: params.displayName,
842
- pubKeyCredParams: [
843
- { alg: -7, type: "public-key" },
844
- { alg: -257, type: "public-key" }
845
- ],
846
- authenticatorSelection: {
847
- authenticatorAttachment: "platform",
848
- residentKey: "preferred",
849
- userVerification: "required"
850
- },
851
- timeout: 6e4
852
- });
889
+ try {
890
+ await waitForDocumentFocus();
891
+ const credential2 = await navigator.credentials.create({
892
+ publicKey: publicKeyOptions
893
+ });
894
+ if (!credential2) {
895
+ throw new Error("Passkey creation was cancelled.");
896
+ }
897
+ return extractPasskeyResult(credential2);
898
+ } catch (err) {
899
+ if (err instanceof PasskeyIframeBlockedError) throw err;
900
+ if (err instanceof Error && err.message === "Passkey creation was cancelled.") throw err;
901
+ throw new PasskeyIframeBlockedError();
902
+ }
853
903
  }
854
904
  await waitForDocumentFocus();
855
905
  const credential = await navigator.credentials.create({
856
- publicKey: {
857
- challenge,
858
- rp: { name: "Swype", id: rpId },
859
- user: {
860
- id: new TextEncoder().encode(params.userId),
861
- name: params.displayName,
862
- displayName: params.displayName
863
- },
864
- pubKeyCredParams: [
865
- { alg: -7, type: "public-key" },
866
- // ES256 (P-256)
867
- { alg: -257, type: "public-key" }
868
- // RS256
869
- ],
870
- authenticatorSelection: {
871
- authenticatorAttachment: "platform",
872
- residentKey: "preferred",
873
- userVerification: "required"
874
- },
875
- timeout: 6e4
876
- }
906
+ publicKey: publicKeyOptions
877
907
  });
878
908
  if (!credential) {
879
909
  throw new Error("Passkey creation was cancelled.");
880
910
  }
911
+ return extractPasskeyResult(credential);
912
+ }
913
+ function extractPasskeyResult(credential) {
881
914
  const response = credential.response;
882
915
  const publicKeyBytes = response.getPublicKey?.();
883
916
  return {
@@ -885,6 +918,29 @@ async function createPasskeyCredential(params) {
885
918
  publicKey: publicKeyBytes ? toBase64(publicKeyBytes) : ""
886
919
  };
887
920
  }
921
+ function buildPasskeyPopupOptions(params) {
922
+ const challenge = new Uint8Array(32);
923
+ crypto.getRandomValues(challenge);
924
+ const rpId = resolvePasskeyRpId();
925
+ return {
926
+ challenge: toBase64(challenge),
927
+ rpId,
928
+ rpName: "Swype",
929
+ userId: toBase64(new TextEncoder().encode(params.userId)),
930
+ userName: params.displayName,
931
+ userDisplayName: params.displayName,
932
+ pubKeyCredParams: [
933
+ { alg: -7, type: "public-key" },
934
+ { alg: -257, type: "public-key" }
935
+ ],
936
+ authenticatorSelection: {
937
+ authenticatorAttachment: "platform",
938
+ residentKey: "preferred",
939
+ userVerification: "required"
940
+ },
941
+ timeout: 6e4
942
+ };
943
+ }
888
944
  async function deviceHasPasskey(credentialId) {
889
945
  const found = await findDevicePasskey([credentialId]);
890
946
  return found != null;
@@ -2647,18 +2703,20 @@ var hintStyle = (color) => ({
2647
2703
  });
2648
2704
  function CreatePasskeyScreen({
2649
2705
  onCreatePasskey,
2650
- onSkip,
2651
2706
  onBack,
2652
2707
  creating,
2653
- error
2708
+ error,
2709
+ popupFallback = false,
2710
+ onCreatePasskeyViaPopup
2654
2711
  }) {
2655
2712
  const { tokens } = useSwypeConfig();
2713
+ const handleCreate = popupFallback && onCreatePasskeyViaPopup ? onCreatePasskeyViaPopup : onCreatePasskey;
2714
+ const buttonLabel = popupFallback ? "Open passkey setup" : "Create passkey";
2656
2715
  return /* @__PURE__ */ jsxRuntime.jsxs(
2657
2716
  ScreenLayout,
2658
2717
  {
2659
2718
  footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2660
- /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { onClick: onCreatePasskey, disabled: creating, loading: creating, children: "Create passkey" }),
2661
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onSkip, style: skipStyle(tokens.textMuted), disabled: creating, children: "Skip for now" }),
2719
+ /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { onClick: handleCreate, disabled: creating, loading: creating, children: buttonLabel }),
2662
2720
  /* @__PURE__ */ jsxRuntime.jsx(PoweredByFooter, {})
2663
2721
  ] }),
2664
2722
  children: [
@@ -2671,7 +2729,7 @@ function CreatePasskeyScreen({
2671
2729
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 14c0 1.5 1.34 2.5 3 2.5s3-1 3-2.5", stroke: tokens.accent, strokeWidth: "1.2", strokeLinecap: "round" })
2672
2730
  ] }) }),
2673
2731
  /* @__PURE__ */ jsxRuntime.jsx("h2", { style: headingStyle3(tokens.text), children: "Create your passkey" }),
2674
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: subtitleStyle3(tokens.textSecondary), children: "Use Face ID to sign in instantly \u2014 no passwords, no codes." }),
2732
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: subtitleStyle3(tokens.textSecondary), children: popupFallback ? "Your browser requires a separate window for passkey setup. Tap the button below to continue." : "Use Face ID to sign in instantly \u2014 no passwords, no codes." }),
2675
2733
  error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: errorBannerStyle2(tokens), children: error }),
2676
2734
  /* @__PURE__ */ jsxRuntime.jsx(InfoBanner, { children: "Your passkey is stored securely on your device. Swype never sees your biometric data." })
2677
2735
  ] })
@@ -2713,19 +2771,6 @@ var errorBannerStyle2 = (tokens) => ({
2713
2771
  width: "100%",
2714
2772
  textAlign: "left"
2715
2773
  });
2716
- var skipStyle = (color) => ({
2717
- background: "transparent",
2718
- border: "none",
2719
- color,
2720
- cursor: "pointer",
2721
- fontFamily: "inherit",
2722
- fontSize: "0.88rem",
2723
- fontWeight: 500,
2724
- display: "block",
2725
- width: "100%",
2726
- textAlign: "center",
2727
- padding: "12px 0 0"
2728
- });
2729
2774
  var WALLET_EMOJIS = {
2730
2775
  rabby: "\u{1F430}",
2731
2776
  ora: "\u2666\uFE0F",
@@ -3944,6 +3989,74 @@ var hintStyle3 = (color) => ({
3944
3989
  color,
3945
3990
  margin: "8px 0 0"
3946
3991
  });
3992
+ var PaymentErrorBoundary = class extends react.Component {
3993
+ constructor(props) {
3994
+ super(props);
3995
+ this.state = { hasError: false };
3996
+ }
3997
+ static getDerivedStateFromError() {
3998
+ return { hasError: true };
3999
+ }
4000
+ componentDidCatch(error, info) {
4001
+ console.error("[SwypePayment] Uncaught error:", error, info.componentStack);
4002
+ }
4003
+ handleReset = () => {
4004
+ this.setState({ hasError: false });
4005
+ this.props.onReset();
4006
+ };
4007
+ render() {
4008
+ if (!this.state.hasError) {
4009
+ return this.props.children;
4010
+ }
4011
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyle8, children: [
4012
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: iconStyle4, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", children: [
4013
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "#ef4444", strokeWidth: "1.5" }),
4014
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 8v5", stroke: "#ef4444", strokeWidth: "1.5", strokeLinecap: "round" }),
4015
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "16", r: "0.75", fill: "#ef4444" })
4016
+ ] }) }),
4017
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: headingStyle9, children: "Something went wrong" }),
4018
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: messageStyle, children: "An unexpected error occurred. Please try again." }),
4019
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: this.handleReset, style: buttonStyle3, children: "Try again" })
4020
+ ] });
4021
+ }
4022
+ };
4023
+ var containerStyle8 = {
4024
+ display: "flex",
4025
+ flexDirection: "column",
4026
+ alignItems: "center",
4027
+ justifyContent: "center",
4028
+ textAlign: "center",
4029
+ padding: "48px 24px",
4030
+ minHeight: "100%",
4031
+ maxWidth: 420,
4032
+ margin: "0 auto"
4033
+ };
4034
+ var iconStyle4 = {
4035
+ marginBottom: 20
4036
+ };
4037
+ var headingStyle9 = {
4038
+ fontSize: "1.25rem",
4039
+ fontWeight: 700,
4040
+ color: "#1a1a1a",
4041
+ margin: "0 0 8px"
4042
+ };
4043
+ var messageStyle = {
4044
+ fontSize: "0.9rem",
4045
+ color: "#666",
4046
+ margin: "0 0 28px",
4047
+ lineHeight: 1.5
4048
+ };
4049
+ var buttonStyle3 = {
4050
+ background: "#1a1a1a",
4051
+ color: "#fff",
4052
+ border: "none",
4053
+ borderRadius: 12,
4054
+ padding: "12px 32px",
4055
+ fontSize: "0.95rem",
4056
+ fontWeight: 600,
4057
+ fontFamily: "inherit",
4058
+ cursor: "pointer"
4059
+ };
3947
4060
  function isInIframe() {
3948
4061
  try {
3949
4062
  return typeof window !== "undefined" && window !== window.top;
@@ -4021,7 +4134,14 @@ function buildSelectSourceChoices(options) {
4021
4134
  }
4022
4135
  return chainChoices;
4023
4136
  }
4024
- function SwypePayment({
4137
+ function SwypePayment(props) {
4138
+ const resetKey = react.useRef(0);
4139
+ const handleBoundaryReset = react.useCallback(() => {
4140
+ resetKey.current += 1;
4141
+ }, []);
4142
+ return /* @__PURE__ */ jsxRuntime.jsx(PaymentErrorBoundary, { onReset: handleBoundaryReset, children: /* @__PURE__ */ jsxRuntime.jsx(SwypePaymentInner, { ...props }) }, resetKey.current);
4143
+ }
4144
+ function SwypePaymentInner({
4025
4145
  destination,
4026
4146
  onComplete,
4027
4147
  onError,
@@ -4062,6 +4182,7 @@ function SwypePayment({
4062
4182
  const [transfer, setTransfer] = react.useState(null);
4063
4183
  const [creatingTransfer, setCreatingTransfer] = react.useState(false);
4064
4184
  const [registeringPasskey, setRegisteringPasskey] = react.useState(false);
4185
+ const [passkeyPopupNeeded, setPasskeyPopupNeeded] = react.useState(false);
4065
4186
  const [activeCredentialId, setActiveCredentialId] = react.useState(() => {
4066
4187
  if (typeof window === "undefined") return null;
4067
4188
  return window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
@@ -4212,7 +4333,7 @@ function SwypePayment({
4212
4333
  }
4213
4334
  setStep("create-passkey");
4214
4335
  } catch {
4215
- if (!cancelled) setStep("deposit");
4336
+ if (!cancelled) setStep("create-passkey");
4216
4337
  }
4217
4338
  };
4218
4339
  checkPasskey();
@@ -4564,44 +4685,59 @@ function SwypePayment({
4564
4685
  merchantAuthorization,
4565
4686
  transfer
4566
4687
  ]);
4688
+ const completePasskeyRegistration = react.useCallback(async (credentialId, publicKey) => {
4689
+ const token = await getAccessToken();
4690
+ if (!token) throw new Error("Not authenticated");
4691
+ await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
4692
+ setActiveCredentialId(credentialId);
4693
+ window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
4694
+ setPasskeyPopupNeeded(false);
4695
+ const hasActiveWallet = accounts.some(
4696
+ (a) => a.wallets.some((w) => w.status === "ACTIVE")
4697
+ );
4698
+ if (accounts.length === 0 || !hasActiveWallet) {
4699
+ setStep("wallet-picker");
4700
+ } else {
4701
+ setStep("deposit");
4702
+ }
4703
+ }, [getAccessToken, apiBaseUrl, accounts]);
4567
4704
  const handleRegisterPasskey = react.useCallback(async () => {
4568
4705
  setRegisteringPasskey(true);
4569
4706
  setError(null);
4570
4707
  try {
4571
- const token = await getAccessToken();
4572
- if (!token) throw new Error("Not authenticated");
4573
4708
  const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
4574
4709
  const { credentialId, publicKey } = await createPasskeyCredential({
4575
4710
  userId: user?.id ?? "unknown",
4576
4711
  displayName: passkeyDisplayName
4577
4712
  });
4578
- await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
4579
- setActiveCredentialId(credentialId);
4580
- window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
4581
- const hasActiveWallet = accounts.some(
4582
- (a) => a.wallets.some((w) => w.status === "ACTIVE")
4583
- );
4584
- if (accounts.length === 0 || !hasActiveWallet) {
4585
- setStep("wallet-picker");
4713
+ await completePasskeyRegistration(credentialId, publicKey);
4714
+ } catch (err) {
4715
+ if (err instanceof PasskeyIframeBlockedError) {
4716
+ setPasskeyPopupNeeded(true);
4586
4717
  } else {
4587
- setStep("deposit");
4718
+ setError(err instanceof Error ? err.message : "Failed to register passkey");
4588
4719
  }
4720
+ } finally {
4721
+ setRegisteringPasskey(false);
4722
+ }
4723
+ }, [user, completePasskeyRegistration]);
4724
+ const handleCreatePasskeyViaPopup = react.useCallback(async () => {
4725
+ setRegisteringPasskey(true);
4726
+ setError(null);
4727
+ try {
4728
+ const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
4729
+ const popupOptions = buildPasskeyPopupOptions({
4730
+ userId: user?.id ?? "unknown",
4731
+ displayName: passkeyDisplayName
4732
+ });
4733
+ const { credentialId, publicKey } = await createPasskeyViaPopup(popupOptions);
4734
+ await completePasskeyRegistration(credentialId, publicKey);
4589
4735
  } catch (err) {
4590
4736
  setError(err instanceof Error ? err.message : "Failed to register passkey");
4591
4737
  } finally {
4592
4738
  setRegisteringPasskey(false);
4593
4739
  }
4594
- }, [getAccessToken, user, apiBaseUrl, accounts]);
4595
- const handleSkipPasskey = react.useCallback(() => {
4596
- const hasActiveWallet = accounts.some(
4597
- (a) => a.wallets.some((w) => w.status === "ACTIVE")
4598
- );
4599
- if (accounts.length === 0 || !hasActiveWallet) {
4600
- setStep("wallet-picker");
4601
- } else {
4602
- setStep("deposit");
4603
- }
4604
- }, [accounts]);
4740
+ }, [user, completePasskeyRegistration]);
4605
4741
  const handleSelectProvider = react.useCallback((providerId) => {
4606
4742
  setSelectedProviderId(providerId);
4607
4743
  setSelectedAccountId(null);
@@ -4717,10 +4853,11 @@ function SwypePayment({
4717
4853
  CreatePasskeyScreen,
4718
4854
  {
4719
4855
  onCreatePasskey: handleRegisterPasskey,
4720
- onSkip: handleSkipPasskey,
4721
- onBack: () => setStep("login"),
4856
+ onBack: handleLogout,
4722
4857
  creating: registeringPasskey,
4723
- error
4858
+ error,
4859
+ popupFallback: passkeyPopupNeeded,
4860
+ onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup
4724
4861
  }
4725
4862
  );
4726
4863
  }
@@ -4733,7 +4870,7 @@ function SwypePayment({
4733
4870
  loading: creatingTransfer,
4734
4871
  onSelectProvider: handleSelectProvider,
4735
4872
  onContinueConnection: handleContinueConnection,
4736
- onBack: () => setStep("create-passkey")
4873
+ onBack: () => setStep(activeCredentialId ? "deposit" : "create-passkey")
4737
4874
  }
4738
4875
  );
4739
4876
  }
@@ -4855,6 +4992,7 @@ function SwypePayment({
4855
4992
 
4856
4993
  exports.IconCircle = IconCircle;
4857
4994
  exports.OutlineButton = OutlineButton;
4995
+ exports.PasskeyIframeBlockedError = PasskeyIframeBlockedError;
4858
4996
  exports.PoweredByFooter = PoweredByFooter;
4859
4997
  exports.PrimaryButton = PrimaryButton;
4860
4998
  exports.ScreenHeader = ScreenHeader;
@@ -4866,7 +5004,9 @@ exports.Spinner = Spinner;
4866
5004
  exports.StepList = StepList;
4867
5005
  exports.SwypePayment = SwypePayment;
4868
5006
  exports.SwypeProvider = SwypeProvider;
5007
+ exports.buildPasskeyPopupOptions = buildPasskeyPopupOptions;
4869
5008
  exports.createPasskeyCredential = createPasskeyCredential;
5009
+ exports.createPasskeyViaPopup = createPasskeyViaPopup;
4870
5010
  exports.darkTheme = darkTheme;
4871
5011
  exports.deviceHasPasskey = deviceHasPasskey;
4872
5012
  exports.findDevicePasskey = findDevicePasskey;