@swype-org/react-sdk 0.1.49 → 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,19 +675,18 @@ function isInCrossOriginIframe() {
669
675
  }
670
676
  }
671
677
  var delegationCounter = 0;
672
- var DELEGATION_CREATE_TIMEOUT_MS = 6e4;
673
678
  var DELEGATION_GET_TIMEOUT_MS = 3e4;
674
- function delegatePasskeyCreate(options) {
679
+ function delegatePasskeyGet(options) {
675
680
  return new Promise((resolve, reject) => {
676
- const id = `pc-${++delegationCounter}-${Date.now()}`;
681
+ const id = `pg-${++delegationCounter}-${Date.now()}`;
677
682
  const timer = setTimeout(() => {
678
683
  window.removeEventListener("message", handler);
679
- reject(new Error("Passkey creation timed out. Please try again."));
680
- }, DELEGATION_CREATE_TIMEOUT_MS);
684
+ reject(new Error("Passkey verification timed out. Please try again."));
685
+ }, DELEGATION_GET_TIMEOUT_MS);
681
686
  const handler = (event) => {
682
687
  const data = event.data;
683
688
  if (!data || typeof data !== "object") return;
684
- if (data.type !== "swype:passkey-create-response" || data.id !== id) return;
689
+ if (data.type !== "swype:passkey-get-response" || data.id !== id) return;
685
690
  clearTimeout(timer);
686
691
  window.removeEventListener("message", handler);
687
692
  if (data.error) {
@@ -689,36 +694,54 @@ function delegatePasskeyCreate(options) {
689
694
  } else if (data.result) {
690
695
  resolve(data.result);
691
696
  } else {
692
- reject(new Error("Invalid passkey create response."));
697
+ reject(new Error("Invalid passkey get response."));
693
698
  }
694
699
  };
695
700
  window.addEventListener("message", handler);
696
- window.parent.postMessage({ type: "swype:passkey-create-request", id, options }, "*");
701
+ window.parent.postMessage({ type: "swype:passkey-get-request", id, options }, "*");
697
702
  });
698
703
  }
699
- function delegatePasskeyGet(options) {
704
+ var POPUP_RESULT_TIMEOUT_MS = 12e4;
705
+ var POPUP_CLOSED_POLL_MS = 500;
706
+ function createPasskeyViaPopup(options) {
700
707
  return new Promise((resolve, reject) => {
701
- 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
+ }
702
715
  const timer = setTimeout(() => {
703
- window.removeEventListener("message", handler);
704
- reject(new Error("Passkey verification timed out. Please try again."));
705
- }, DELEGATION_GET_TIMEOUT_MS);
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);
706
725
  const handler = (event) => {
726
+ if (event.source !== popup) return;
707
727
  const data = event.data;
708
728
  if (!data || typeof data !== "object") return;
709
- if (data.type !== "swype:passkey-get-response" || data.id !== id) return;
710
- clearTimeout(timer);
711
- window.removeEventListener("message", handler);
729
+ if (data.type !== "swype:passkey-popup-result") return;
730
+ cleanup();
712
731
  if (data.error) {
713
732
  reject(new Error(data.error));
714
733
  } else if (data.result) {
715
734
  resolve(data.result);
716
735
  } else {
717
- reject(new Error("Invalid passkey get response."));
736
+ reject(new Error("Invalid passkey popup response."));
718
737
  }
719
738
  };
739
+ function cleanup() {
740
+ clearTimeout(timer);
741
+ clearInterval(closedPoll);
742
+ window.removeEventListener("message", handler);
743
+ }
720
744
  window.addEventListener("message", handler);
721
- window.parent.postMessage({ type: "swype:passkey-get-request", id, options }, "*");
722
745
  });
723
746
  }
724
747
 
@@ -843,53 +866,51 @@ async function createPasskeyCredential(params) {
843
866
  const challenge = new Uint8Array(32);
844
867
  crypto.getRandomValues(challenge);
845
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
+ };
846
888
  if (isInCrossOriginIframe()) {
847
- return delegatePasskeyCreate({
848
- challenge: toBase64(challenge),
849
- rpId,
850
- rpName: "Swype",
851
- userId: toBase64(new TextEncoder().encode(params.userId)),
852
- userName: params.displayName,
853
- userDisplayName: params.displayName,
854
- pubKeyCredParams: [
855
- { alg: -7, type: "public-key" },
856
- { alg: -257, type: "public-key" }
857
- ],
858
- authenticatorSelection: {
859
- authenticatorAttachment: "platform",
860
- residentKey: "preferred",
861
- userVerification: "required"
862
- },
863
- timeout: 6e4
864
- });
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
+ }
865
903
  }
866
904
  await waitForDocumentFocus();
867
905
  const credential = await navigator.credentials.create({
868
- publicKey: {
869
- challenge,
870
- rp: { name: "Swype", id: rpId },
871
- user: {
872
- id: new TextEncoder().encode(params.userId),
873
- name: params.displayName,
874
- displayName: params.displayName
875
- },
876
- pubKeyCredParams: [
877
- { alg: -7, type: "public-key" },
878
- // ES256 (P-256)
879
- { alg: -257, type: "public-key" }
880
- // RS256
881
- ],
882
- authenticatorSelection: {
883
- authenticatorAttachment: "platform",
884
- residentKey: "preferred",
885
- userVerification: "required"
886
- },
887
- timeout: 6e4
888
- }
906
+ publicKey: publicKeyOptions
889
907
  });
890
908
  if (!credential) {
891
909
  throw new Error("Passkey creation was cancelled.");
892
910
  }
911
+ return extractPasskeyResult(credential);
912
+ }
913
+ function extractPasskeyResult(credential) {
893
914
  const response = credential.response;
894
915
  const publicKeyBytes = response.getPublicKey?.();
895
916
  return {
@@ -897,6 +918,29 @@ async function createPasskeyCredential(params) {
897
918
  publicKey: publicKeyBytes ? toBase64(publicKeyBytes) : ""
898
919
  };
899
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
+ }
900
944
  async function deviceHasPasskey(credentialId) {
901
945
  const found = await findDevicePasskey([credentialId]);
902
946
  return found != null;
@@ -2661,14 +2705,18 @@ function CreatePasskeyScreen({
2661
2705
  onCreatePasskey,
2662
2706
  onBack,
2663
2707
  creating,
2664
- error
2708
+ error,
2709
+ popupFallback = false,
2710
+ onCreatePasskeyViaPopup
2665
2711
  }) {
2666
2712
  const { tokens } = useSwypeConfig();
2713
+ const handleCreate = popupFallback && onCreatePasskeyViaPopup ? onCreatePasskeyViaPopup : onCreatePasskey;
2714
+ const buttonLabel = popupFallback ? "Open passkey setup" : "Create passkey";
2667
2715
  return /* @__PURE__ */ jsxRuntime.jsxs(
2668
2716
  ScreenLayout,
2669
2717
  {
2670
2718
  footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2671
- /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { onClick: onCreatePasskey, disabled: creating, loading: creating, children: "Create passkey" }),
2719
+ /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { onClick: handleCreate, disabled: creating, loading: creating, children: buttonLabel }),
2672
2720
  /* @__PURE__ */ jsxRuntime.jsx(PoweredByFooter, {})
2673
2721
  ] }),
2674
2722
  children: [
@@ -2681,7 +2729,7 @@ function CreatePasskeyScreen({
2681
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" })
2682
2730
  ] }) }),
2683
2731
  /* @__PURE__ */ jsxRuntime.jsx("h2", { style: headingStyle3(tokens.text), children: "Create your passkey" }),
2684
- /* @__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." }),
2685
2733
  error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: errorBannerStyle2(tokens), children: error }),
2686
2734
  /* @__PURE__ */ jsxRuntime.jsx(InfoBanner, { children: "Your passkey is stored securely on your device. Swype never sees your biometric data." })
2687
2735
  ] })
@@ -4134,6 +4182,7 @@ function SwypePaymentInner({
4134
4182
  const [transfer, setTransfer] = react.useState(null);
4135
4183
  const [creatingTransfer, setCreatingTransfer] = react.useState(false);
4136
4184
  const [registeringPasskey, setRegisteringPasskey] = react.useState(false);
4185
+ const [passkeyPopupNeeded, setPasskeyPopupNeeded] = react.useState(false);
4137
4186
  const [activeCredentialId, setActiveCredentialId] = react.useState(() => {
4138
4187
  if (typeof window === "undefined") return null;
4139
4188
  return window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
@@ -4636,34 +4685,59 @@ function SwypePaymentInner({
4636
4685
  merchantAuthorization,
4637
4686
  transfer
4638
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]);
4639
4704
  const handleRegisterPasskey = react.useCallback(async () => {
4640
4705
  setRegisteringPasskey(true);
4641
4706
  setError(null);
4642
4707
  try {
4643
- const token = await getAccessToken();
4644
- if (!token) throw new Error("Not authenticated");
4645
4708
  const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
4646
4709
  const { credentialId, publicKey } = await createPasskeyCredential({
4647
4710
  userId: user?.id ?? "unknown",
4648
4711
  displayName: passkeyDisplayName
4649
4712
  });
4650
- await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
4651
- setActiveCredentialId(credentialId);
4652
- window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
4653
- const hasActiveWallet = accounts.some(
4654
- (a) => a.wallets.some((w) => w.status === "ACTIVE")
4655
- );
4656
- if (accounts.length === 0 || !hasActiveWallet) {
4657
- setStep("wallet-picker");
4713
+ await completePasskeyRegistration(credentialId, publicKey);
4714
+ } catch (err) {
4715
+ if (err instanceof PasskeyIframeBlockedError) {
4716
+ setPasskeyPopupNeeded(true);
4658
4717
  } else {
4659
- setStep("deposit");
4718
+ setError(err instanceof Error ? err.message : "Failed to register passkey");
4660
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);
4661
4735
  } catch (err) {
4662
4736
  setError(err instanceof Error ? err.message : "Failed to register passkey");
4663
4737
  } finally {
4664
4738
  setRegisteringPasskey(false);
4665
4739
  }
4666
- }, [getAccessToken, user, apiBaseUrl, accounts]);
4740
+ }, [user, completePasskeyRegistration]);
4667
4741
  const handleSelectProvider = react.useCallback((providerId) => {
4668
4742
  setSelectedProviderId(providerId);
4669
4743
  setSelectedAccountId(null);
@@ -4781,7 +4855,9 @@ function SwypePaymentInner({
4781
4855
  onCreatePasskey: handleRegisterPasskey,
4782
4856
  onBack: handleLogout,
4783
4857
  creating: registeringPasskey,
4784
- error
4858
+ error,
4859
+ popupFallback: passkeyPopupNeeded,
4860
+ onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup
4785
4861
  }
4786
4862
  );
4787
4863
  }
@@ -4916,6 +4992,7 @@ function SwypePaymentInner({
4916
4992
 
4917
4993
  exports.IconCircle = IconCircle;
4918
4994
  exports.OutlineButton = OutlineButton;
4995
+ exports.PasskeyIframeBlockedError = PasskeyIframeBlockedError;
4919
4996
  exports.PoweredByFooter = PoweredByFooter;
4920
4997
  exports.PrimaryButton = PrimaryButton;
4921
4998
  exports.ScreenHeader = ScreenHeader;
@@ -4927,7 +5004,9 @@ exports.Spinner = Spinner;
4927
5004
  exports.StepList = StepList;
4928
5005
  exports.SwypePayment = SwypePayment;
4929
5006
  exports.SwypeProvider = SwypeProvider;
5007
+ exports.buildPasskeyPopupOptions = buildPasskeyPopupOptions;
4930
5008
  exports.createPasskeyCredential = createPasskeyCredential;
5009
+ exports.createPasskeyViaPopup = createPasskeyViaPopup;
4931
5010
  exports.darkTheme = darkTheme;
4932
5011
  exports.deviceHasPasskey = deviceHasPasskey;
4933
5012
  exports.findDevicePasskey = findDevicePasskey;