@swype-org/react-sdk 0.1.86 → 0.1.88

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.d.cts CHANGED
@@ -189,11 +189,13 @@ interface UserConfig {
189
189
  passkey?: {
190
190
  credentialId: string;
191
191
  publicKey: string;
192
+ lastVerificationToken?: string | null;
192
193
  } | null;
193
194
  /** All registered WebAuthn passkey credentials for this user */
194
195
  passkeys?: {
195
196
  credentialId: string;
196
197
  publicKey: string;
198
+ lastVerificationToken?: string | null;
197
199
  }[];
198
200
  }
199
201
  /** Theme mode */
@@ -389,19 +391,16 @@ interface SwypePaymentProps {
389
391
  declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.Element;
390
392
 
391
393
  /**
392
- * Cross-origin iframe passkey delegation via postMessage.
394
+ * Cross-origin iframe passkey helpers.
393
395
  *
394
396
  * When the webview-app runs inside a cross-origin iframe, WebAuthn
395
397
  * ceremonies cannot execute from within the iframe on Safari. For passkey
396
398
  * creation, we first attempt a direct `navigator.credentials.create()`
397
399
  * call (works in Chrome/Firefox with the iframe permissions policy). If
398
400
  * that fails (Safari), we open a same-origin pop-up window on the Swype
399
- * domain to perform the ceremony, then relay the result back via
400
- * `BroadcastChannel` (Safari strips `window.opener` from popups opened
401
- * by cross-origin iframes, so `postMessage` via opener doesn't work).
402
- *
403
- * Passkey *assertion* (`navigator.credentials.get`) still delegates to
404
- * the parent page via postMessage as before.
401
+ * domain to perform the ceremony. The popup writes the result to the
402
+ * server with a verification token, and the opener reads from the server
403
+ * after the popup closes.
405
404
  */
406
405
  /**
407
406
  * Thrown when `navigator.credentials.create()` fails inside a
@@ -429,7 +428,7 @@ interface PasskeyPopupOptions {
429
428
  };
430
429
  timeout?: number;
431
430
  /** Populated by `createPasskeyViaPopup`; not set by callers. */
432
- channelId?: string;
431
+ verificationToken?: string;
433
432
  /** Privy JWT so the popup can register the passkey server-side. */
434
433
  authToken?: string;
435
434
  /** Core API base URL for server-side passkey registration. */
@@ -440,24 +439,14 @@ interface PasskeyPopupOptions {
440
439
  * passkey creation. Used as a fallback when Safari blocks
441
440
  * `navigator.credentials.create()` inside a cross-origin iframe.
442
441
  *
443
- * Communication uses `BroadcastChannel` because Safari strips
444
- * `window.opener` from popups opened by cross-origin iframes.
445
- * Falls back to `window.postMessage` for browsers that preserve the
446
- * opener reference.
447
- *
448
- * When both client-side channels are blocked (Safari ITP partitions
449
- * BroadcastChannel by top-level origin), the popup registers the
450
- * passkey directly with the backend. On popup close, this function
451
- * checks the server for newly registered passkeys as a final fallback.
442
+ * The popup registers the passkey with the server using a verification
443
+ * token. After the popup closes, this function checks the server for
444
+ * the passkey matching the token.
452
445
  *
453
446
  * Must be called from a user-gesture handler (e.g. button click) to
454
447
  * avoid the browser's pop-up blocker.
455
- *
456
- * @param existingCredentialIds - Credential IDs known before the popup
457
- * opens. Used to diff against server state when the popup closes
458
- * without delivering a client-side result.
459
448
  */
460
- declare function createPasskeyViaPopup(options: PasskeyPopupOptions, existingCredentialIds?: string[]): Promise<{
449
+ declare function createPasskeyViaPopup(options: PasskeyPopupOptions): Promise<{
461
450
  credentialId: string;
462
451
  publicKey: string;
463
452
  }>;
@@ -465,7 +454,11 @@ interface PasskeyVerifyPopupOptions {
465
454
  credentialIds: string[];
466
455
  rpId: string;
467
456
  /** Populated by `findDevicePasskeyViaPopup`; not set by callers. */
468
- channelId?: string;
457
+ verificationToken?: string;
458
+ /** Privy JWT so the popup can report verification server-side. */
459
+ authToken?: string;
460
+ /** Core API base URL for server-side passkey activity reporting. */
461
+ apiBaseUrl?: string;
469
462
  }
470
463
  /**
471
464
  * Opens a same-origin pop-up window on the Swype domain to check whether
@@ -475,6 +468,10 @@ interface PasskeyVerifyPopupOptions {
475
468
  * inside a cross-origin iframe. The popup runs on the Swype domain where
476
469
  * the rpId matches, so WebAuthn works.
477
470
  *
471
+ * The popup writes a verification token to the server. After the popup
472
+ * closes, the opener checks the server for the matching token to determine
473
+ * which credential was verified.
474
+ *
478
475
  * Must be called from a user-gesture handler (e.g. button click) to
479
476
  * avoid the browser's pop-up blocker.
480
477
  *
package/dist/index.d.ts CHANGED
@@ -189,11 +189,13 @@ interface UserConfig {
189
189
  passkey?: {
190
190
  credentialId: string;
191
191
  publicKey: string;
192
+ lastVerificationToken?: string | null;
192
193
  } | null;
193
194
  /** All registered WebAuthn passkey credentials for this user */
194
195
  passkeys?: {
195
196
  credentialId: string;
196
197
  publicKey: string;
198
+ lastVerificationToken?: string | null;
197
199
  }[];
198
200
  }
199
201
  /** Theme mode */
@@ -389,19 +391,16 @@ interface SwypePaymentProps {
389
391
  declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.Element;
390
392
 
391
393
  /**
392
- * Cross-origin iframe passkey delegation via postMessage.
394
+ * Cross-origin iframe passkey helpers.
393
395
  *
394
396
  * When the webview-app runs inside a cross-origin iframe, WebAuthn
395
397
  * ceremonies cannot execute from within the iframe on Safari. For passkey
396
398
  * creation, we first attempt a direct `navigator.credentials.create()`
397
399
  * call (works in Chrome/Firefox with the iframe permissions policy). If
398
400
  * that fails (Safari), we open a same-origin pop-up window on the Swype
399
- * domain to perform the ceremony, then relay the result back via
400
- * `BroadcastChannel` (Safari strips `window.opener` from popups opened
401
- * by cross-origin iframes, so `postMessage` via opener doesn't work).
402
- *
403
- * Passkey *assertion* (`navigator.credentials.get`) still delegates to
404
- * the parent page via postMessage as before.
401
+ * domain to perform the ceremony. The popup writes the result to the
402
+ * server with a verification token, and the opener reads from the server
403
+ * after the popup closes.
405
404
  */
406
405
  /**
407
406
  * Thrown when `navigator.credentials.create()` fails inside a
@@ -429,7 +428,7 @@ interface PasskeyPopupOptions {
429
428
  };
430
429
  timeout?: number;
431
430
  /** Populated by `createPasskeyViaPopup`; not set by callers. */
432
- channelId?: string;
431
+ verificationToken?: string;
433
432
  /** Privy JWT so the popup can register the passkey server-side. */
434
433
  authToken?: string;
435
434
  /** Core API base URL for server-side passkey registration. */
@@ -440,24 +439,14 @@ interface PasskeyPopupOptions {
440
439
  * passkey creation. Used as a fallback when Safari blocks
441
440
  * `navigator.credentials.create()` inside a cross-origin iframe.
442
441
  *
443
- * Communication uses `BroadcastChannel` because Safari strips
444
- * `window.opener` from popups opened by cross-origin iframes.
445
- * Falls back to `window.postMessage` for browsers that preserve the
446
- * opener reference.
447
- *
448
- * When both client-side channels are blocked (Safari ITP partitions
449
- * BroadcastChannel by top-level origin), the popup registers the
450
- * passkey directly with the backend. On popup close, this function
451
- * checks the server for newly registered passkeys as a final fallback.
442
+ * The popup registers the passkey with the server using a verification
443
+ * token. After the popup closes, this function checks the server for
444
+ * the passkey matching the token.
452
445
  *
453
446
  * Must be called from a user-gesture handler (e.g. button click) to
454
447
  * avoid the browser's pop-up blocker.
455
- *
456
- * @param existingCredentialIds - Credential IDs known before the popup
457
- * opens. Used to diff against server state when the popup closes
458
- * without delivering a client-side result.
459
448
  */
460
- declare function createPasskeyViaPopup(options: PasskeyPopupOptions, existingCredentialIds?: string[]): Promise<{
449
+ declare function createPasskeyViaPopup(options: PasskeyPopupOptions): Promise<{
461
450
  credentialId: string;
462
451
  publicKey: string;
463
452
  }>;
@@ -465,7 +454,11 @@ interface PasskeyVerifyPopupOptions {
465
454
  credentialIds: string[];
466
455
  rpId: string;
467
456
  /** Populated by `findDevicePasskeyViaPopup`; not set by callers. */
468
- channelId?: string;
457
+ verificationToken?: string;
458
+ /** Privy JWT so the popup can report verification server-side. */
459
+ authToken?: string;
460
+ /** Core API base URL for server-side passkey activity reporting. */
461
+ apiBaseUrl?: string;
469
462
  }
470
463
  /**
471
464
  * Opens a same-origin pop-up window on the Swype domain to check whether
@@ -475,6 +468,10 @@ interface PasskeyVerifyPopupOptions {
475
468
  * inside a cross-origin iframe. The popup runs on the Swype domain where
476
469
  * the rpId matches, so WebAuthn works.
477
470
  *
471
+ * The popup writes a verification token to the server. After the popup
472
+ * closes, the opener checks the server for the matching token to determine
473
+ * which credential was verified.
474
+ *
478
475
  * Must be called from a user-gesture handler (e.g. button click) to
479
476
  * avoid the browser's pop-up blocker.
480
477
  *
package/dist/index.js CHANGED
@@ -725,10 +725,10 @@ function isSafari() {
725
725
  var POPUP_RESULT_TIMEOUT_MS = 12e4;
726
726
  var POPUP_CLOSED_POLL_MS = 500;
727
727
  var POPUP_CLOSED_GRACE_MS = 1e3;
728
- function createPasskeyViaPopup(options, existingCredentialIds = []) {
728
+ function createPasskeyViaPopup(options) {
729
729
  return new Promise((resolve, reject) => {
730
- const channelId = `swype-pk-${Date.now()}-${Math.random().toString(36).slice(2)}`;
731
- const payload = { ...options, channelId };
730
+ const verificationToken = crypto.randomUUID();
731
+ const payload = { ...options, verificationToken };
732
732
  const encoded = btoa(JSON.stringify(payload));
733
733
  const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
734
734
  const popup = window.open(popupUrl, "swype-passkey");
@@ -737,22 +737,21 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
737
737
  return;
738
738
  }
739
739
  let settled = false;
740
- const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
741
740
  const timer = setTimeout(() => {
742
741
  cleanup();
743
742
  reject(new Error("Passkey creation timed out. Please try again."));
744
743
  }, POPUP_RESULT_TIMEOUT_MS);
745
- let closedGraceTimer = null;
746
744
  const closedPoll = setInterval(() => {
747
745
  if (popup.closed) {
748
746
  clearInterval(closedPoll);
749
- closedGraceTimer = setTimeout(() => {
747
+ setTimeout(() => {
750
748
  if (!settled) {
749
+ settled = true;
751
750
  cleanup();
752
- checkServerForNewPasskey(
751
+ checkServerForPasskeyByToken(
753
752
  options.authToken,
754
753
  options.apiBaseUrl,
755
- existingCredentialIds
754
+ verificationToken
756
755
  ).then((result) => {
757
756
  if (result) {
758
757
  resolve(result);
@@ -766,42 +765,20 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
766
765
  }, POPUP_CLOSED_GRACE_MS);
767
766
  }
768
767
  }, POPUP_CLOSED_POLL_MS);
769
- function handleResult(data) {
770
- if (settled) return;
771
- if (!data || typeof data !== "object") return;
772
- if (data.type !== "swype:passkey-popup-result") return;
773
- settled = true;
774
- cleanup();
775
- if (data.error) {
776
- reject(new Error(data.error));
777
- } else if (data.result) {
778
- resolve(data.result);
779
- } else {
780
- reject(new Error("Invalid passkey popup response."));
781
- }
782
- }
783
- if (channel) {
784
- channel.onmessage = (event) => handleResult(event.data);
785
- }
786
- const postMessageHandler = (event) => {
787
- if (event.source !== popup) return;
788
- handleResult(event.data);
789
- };
790
- window.addEventListener("message", postMessageHandler);
791
768
  function cleanup() {
792
769
  clearTimeout(timer);
793
770
  clearInterval(closedPoll);
794
- if (closedGraceTimer) clearTimeout(closedGraceTimer);
795
- window.removeEventListener("message", postMessageHandler);
796
- channel?.close();
797
771
  }
798
772
  });
799
773
  }
800
774
  var VERIFY_POPUP_TIMEOUT_MS = 6e4;
801
775
  function findDevicePasskeyViaPopup(options) {
802
776
  return new Promise((resolve, reject) => {
803
- const channelId = `swype-pv-${Date.now()}-${Math.random().toString(36).slice(2)}`;
804
- const payload = { ...options, channelId };
777
+ const verificationToken = crypto.randomUUID();
778
+ const payload = {
779
+ ...options,
780
+ verificationToken
781
+ };
805
782
  const encoded = btoa(JSON.stringify(payload));
806
783
  const popupUrl = `${window.location.origin}/passkey-verify#${encoded}`;
807
784
  const popup = window.open(popupUrl, "swype-passkey-verify");
@@ -810,7 +787,6 @@ function findDevicePasskeyViaPopup(options) {
810
787
  return;
811
788
  }
812
789
  let settled = false;
813
- const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
814
790
  const timer = setTimeout(() => {
815
791
  cleanup();
816
792
  resolve(null);
@@ -820,44 +796,28 @@ function findDevicePasskeyViaPopup(options) {
820
796
  clearInterval(closedPoll);
821
797
  setTimeout(() => {
822
798
  if (!settled) {
799
+ settled = true;
823
800
  cleanup();
824
- resolve(null);
801
+ checkServerForPasskeyByToken(
802
+ options.authToken,
803
+ options.apiBaseUrl,
804
+ verificationToken
805
+ ).then((result) => {
806
+ resolve(result?.credentialId ?? null);
807
+ }).catch(() => {
808
+ resolve(null);
809
+ });
825
810
  }
826
811
  }, POPUP_CLOSED_GRACE_MS);
827
812
  }
828
813
  }, POPUP_CLOSED_POLL_MS);
829
- function handleResult(data) {
830
- if (settled) return;
831
- if (!data || typeof data !== "object") return;
832
- if (data.type !== "swype:passkey-verify-result") return;
833
- settled = true;
834
- cleanup();
835
- if (data.error) {
836
- resolve(null);
837
- } else if (data.result && typeof data.result === "object") {
838
- const result = data.result;
839
- resolve(result.credentialId ?? null);
840
- } else {
841
- resolve(null);
842
- }
843
- }
844
- if (channel) {
845
- channel.onmessage = (event) => handleResult(event.data);
846
- }
847
- const postMessageHandler = (event) => {
848
- if (event.source !== popup) return;
849
- handleResult(event.data);
850
- };
851
- window.addEventListener("message", postMessageHandler);
852
814
  function cleanup() {
853
815
  clearTimeout(timer);
854
816
  clearInterval(closedPoll);
855
- window.removeEventListener("message", postMessageHandler);
856
- channel?.close();
857
817
  }
858
818
  });
859
819
  }
860
- async function checkServerForNewPasskey(authToken, apiBaseUrl, existingCredentialIds) {
820
+ async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
861
821
  if (!authToken || !apiBaseUrl) return null;
862
822
  const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
863
823
  headers: { Authorization: `Bearer ${authToken}` }
@@ -865,9 +825,8 @@ async function checkServerForNewPasskey(authToken, apiBaseUrl, existingCredentia
865
825
  if (!res.ok) return null;
866
826
  const body = await res.json();
867
827
  const passkeys = body.config.passkeys ?? [];
868
- const existingSet = new Set(existingCredentialIds);
869
- const newPasskey = passkeys.find((p) => !existingSet.has(p.credentialId));
870
- return newPasskey ?? null;
828
+ const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
829
+ return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
871
830
  }
872
831
 
873
832
  // src/hooks.ts
@@ -5572,30 +5531,42 @@ function SwypePaymentInner({
5572
5531
  authToken: token ?? void 0,
5573
5532
  apiBaseUrl
5574
5533
  });
5575
- const { credentialId, publicKey } = await createPasskeyViaPopup(
5576
- popupOptions,
5577
- knownCredentialIds
5578
- );
5579
- await completePasskeyRegistration(credentialId, publicKey);
5534
+ const { credentialId } = await createPasskeyViaPopup(popupOptions);
5535
+ setActiveCredentialId(credentialId);
5536
+ localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
5537
+ setPasskeyPopupNeeded(false);
5538
+ const resolved = resolvePostAuthStep({
5539
+ hasPasskey: true,
5540
+ accounts,
5541
+ persistedMobileFlow: loadMobileFlowState(),
5542
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5543
+ connectingNewAccount
5544
+ });
5545
+ if (resolved.clearPersistedFlow) {
5546
+ clearMobileFlowState();
5547
+ }
5548
+ setStep(resolved.step);
5580
5549
  } catch (err) {
5581
5550
  captureException(err);
5582
5551
  setError(err instanceof Error ? err.message : "Failed to register passkey");
5583
5552
  } finally {
5584
5553
  setRegisteringPasskey(false);
5585
5554
  }
5586
- }, [user, completePasskeyRegistration, getAccessToken, apiBaseUrl, knownCredentialIds]);
5555
+ }, [user, getAccessToken, apiBaseUrl, accounts, connectingNewAccount]);
5587
5556
  const handleVerifyPasskeyViaPopup = useCallback(async () => {
5588
5557
  setVerifyingPasskeyPopup(true);
5589
5558
  setError(null);
5590
5559
  try {
5560
+ const token = await getAccessToken();
5591
5561
  const matched = await findDevicePasskeyViaPopup({
5592
5562
  credentialIds: knownCredentialIds,
5593
- rpId: resolvePasskeyRpId()
5563
+ rpId: resolvePasskeyRpId(),
5564
+ authToken: token ?? void 0,
5565
+ apiBaseUrl
5594
5566
  });
5595
5567
  if (matched) {
5596
5568
  setActiveCredentialId(matched);
5597
5569
  window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5598
- const token = await getAccessToken();
5599
5570
  if (token) {
5600
5571
  reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
5601
5572
  });