@swype-org/react-sdk 0.1.79 → 0.1.81

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
@@ -389,7 +389,8 @@ declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.E
389
389
  * call (works in Chrome/Firefox with the iframe permissions policy). If
390
390
  * that fails (Safari), we open a same-origin pop-up window on the Swype
391
391
  * domain to perform the ceremony, then relay the result back via
392
- * `postMessage`.
392
+ * `BroadcastChannel` (Safari strips `window.opener` from popups opened
393
+ * by cross-origin iframes, so `postMessage` via opener doesn't work).
393
394
  *
394
395
  * Passkey *assertion* (`navigator.credentials.get`) still delegates to
395
396
  * the parent page via postMessage as before.
@@ -419,12 +420,19 @@ interface PasskeyPopupOptions {
419
420
  userVerification?: string;
420
421
  };
421
422
  timeout?: number;
423
+ /** Populated by `createPasskeyViaPopup`; not set by callers. */
424
+ channelId?: string;
422
425
  }
423
426
  /**
424
427
  * Opens a same-origin pop-up window on the Swype domain to perform
425
428
  * passkey creation. Used as a fallback when Safari blocks
426
429
  * `navigator.credentials.create()` inside a cross-origin iframe.
427
430
  *
431
+ * Communication uses `BroadcastChannel` because Safari strips
432
+ * `window.opener` from popups opened by cross-origin iframes.
433
+ * Falls back to `window.postMessage` for browsers that preserve the
434
+ * opener reference.
435
+ *
428
436
  * Must be called from a user-gesture handler (e.g. button click) to
429
437
  * avoid the browser's pop-up blocker.
430
438
  */
package/dist/index.d.ts CHANGED
@@ -389,7 +389,8 @@ declare function SwypePayment(props: SwypePaymentProps): react_jsx_runtime.JSX.E
389
389
  * call (works in Chrome/Firefox with the iframe permissions policy). If
390
390
  * that fails (Safari), we open a same-origin pop-up window on the Swype
391
391
  * domain to perform the ceremony, then relay the result back via
392
- * `postMessage`.
392
+ * `BroadcastChannel` (Safari strips `window.opener` from popups opened
393
+ * by cross-origin iframes, so `postMessage` via opener doesn't work).
393
394
  *
394
395
  * Passkey *assertion* (`navigator.credentials.get`) still delegates to
395
396
  * the parent page via postMessage as before.
@@ -419,12 +420,19 @@ interface PasskeyPopupOptions {
419
420
  userVerification?: string;
420
421
  };
421
422
  timeout?: number;
423
+ /** Populated by `createPasskeyViaPopup`; not set by callers. */
424
+ channelId?: string;
422
425
  }
423
426
  /**
424
427
  * Opens a same-origin pop-up window on the Swype domain to perform
425
428
  * passkey creation. Used as a fallback when Safari blocks
426
429
  * `navigator.credentials.create()` inside a cross-origin iframe.
427
430
  *
431
+ * Communication uses `BroadcastChannel` because Safari strips
432
+ * `window.opener` from popups opened by cross-origin iframes.
433
+ * Falls back to `window.postMessage` for browsers that preserve the
434
+ * opener reference.
435
+ *
428
436
  * Must be called from a user-gesture handler (e.g. button click) to
429
437
  * avoid the browser's pop-up blocker.
430
438
  */
package/dist/index.js CHANGED
@@ -698,13 +698,17 @@ var POPUP_RESULT_TIMEOUT_MS = 12e4;
698
698
  var POPUP_CLOSED_POLL_MS = 500;
699
699
  function createPasskeyViaPopup(options) {
700
700
  return new Promise((resolve, reject) => {
701
- const encoded = btoa(JSON.stringify(options));
701
+ const channelId = `swype-pk-${Date.now()}-${Math.random().toString(36).slice(2)}`;
702
+ const payload = { ...options, channelId };
703
+ const encoded = btoa(JSON.stringify(payload));
702
704
  const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
703
705
  const popup = window.open(popupUrl, "swype-passkey");
704
706
  if (!popup) {
705
707
  reject(new Error("Pop-up blocked. Please allow pop-ups for this site and try again."));
706
708
  return;
707
709
  }
710
+ let settled = false;
711
+ const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
708
712
  const timer = setTimeout(() => {
709
713
  cleanup();
710
714
  reject(new Error("Passkey creation timed out. Please try again."));
@@ -715,11 +719,11 @@ function createPasskeyViaPopup(options) {
715
719
  reject(new Error("Passkey setup window was closed before completing."));
716
720
  }
717
721
  }, POPUP_CLOSED_POLL_MS);
718
- const handler = (event) => {
719
- if (event.source !== popup) return;
720
- const data = event.data;
722
+ function handleResult(data) {
723
+ if (settled) return;
721
724
  if (!data || typeof data !== "object") return;
722
725
  if (data.type !== "swype:passkey-popup-result") return;
726
+ settled = true;
723
727
  cleanup();
724
728
  if (data.error) {
725
729
  reject(new Error(data.error));
@@ -728,13 +732,21 @@ function createPasskeyViaPopup(options) {
728
732
  } else {
729
733
  reject(new Error("Invalid passkey popup response."));
730
734
  }
735
+ }
736
+ if (channel) {
737
+ channel.onmessage = (event) => handleResult(event.data);
738
+ }
739
+ const postMessageHandler = (event) => {
740
+ if (event.source !== popup) return;
741
+ handleResult(event.data);
731
742
  };
743
+ window.addEventListener("message", postMessageHandler);
732
744
  function cleanup() {
733
745
  clearTimeout(timer);
734
746
  clearInterval(closedPoll);
735
- window.removeEventListener("message", handler);
747
+ window.removeEventListener("message", postMessageHandler);
748
+ channel?.close();
736
749
  }
737
- window.addEventListener("message", handler);
738
750
  });
739
751
  }
740
752
 
@@ -3244,12 +3256,15 @@ function DepositScreen({
3244
3256
  onChangeSource,
3245
3257
  onSwitchWallet,
3246
3258
  onBack,
3247
- onLogout
3259
+ onLogout,
3260
+ onIncreaseLimit,
3261
+ increasingLimit
3248
3262
  }) {
3249
3263
  const { tokens } = useSwypeConfig();
3250
3264
  const amount = initialAmount;
3251
3265
  const isLowBalance = availableBalance < MIN_DEPOSIT;
3252
- const canDeposit = amount >= MIN_DEPOSIT && amount <= remainingLimit && !isLowBalance && !processing;
3266
+ const exceedsLimit = amount > remainingLimit && !isLowBalance;
3267
+ const canDeposit = amount >= MIN_DEPOSIT && !exceedsLimit && !isLowBalance && !processing;
3253
3268
  const headerTitle = merchantName ? `Deposit to ${merchantName}` : "Deposit";
3254
3269
  if (isLowBalance) {
3255
3270
  return /* @__PURE__ */ jsxs(
@@ -3307,11 +3322,22 @@ function DepositScreen({
3307
3322
  ScreenLayout,
3308
3323
  {
3309
3324
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
3310
- /* @__PURE__ */ jsxs(PrimaryButton, { onClick: () => onDeposit(amount), disabled: !canDeposit, loading: processing, children: [
3311
- "Deposit $",
3312
- amount.toFixed(2)
3325
+ exceedsLimit && onIncreaseLimit ? /* @__PURE__ */ jsxs(Fragment, { children: [
3326
+ /* @__PURE__ */ jsx(PrimaryButton, { onClick: onIncreaseLimit, loading: increasingLimit, children: "Increase limit" }),
3327
+ /* @__PURE__ */ jsxs("p", { style: limitExceededHintStyle(tokens.warning), children: [
3328
+ "Your deposit of $",
3329
+ amount.toFixed(2),
3330
+ " exceeds your One-Tap limit of $",
3331
+ remainingLimit.toFixed(2),
3332
+ ". Increase your limit to continue."
3333
+ ] })
3334
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3335
+ /* @__PURE__ */ jsxs(PrimaryButton, { onClick: () => onDeposit(amount), disabled: !canDeposit, loading: processing, children: [
3336
+ "Deposit $",
3337
+ amount.toFixed(2)
3338
+ ] }),
3339
+ /* @__PURE__ */ jsx("p", { style: noApprovalStyle(tokens.textMuted), children: "No approval needed \xB7 within your One-Tap limit" })
3313
3340
  ] }),
3314
- /* @__PURE__ */ jsx("p", { style: noApprovalStyle(tokens.textMuted), children: "No approval needed \xB7 within your One-Tap limit" }),
3315
3341
  /* @__PURE__ */ jsxs("p", { style: routeStyle(tokens.textMuted), children: [
3316
3342
  "From ",
3317
3343
  sourceName,
@@ -3459,6 +3485,13 @@ var noApprovalStyle = (color) => ({
3459
3485
  color,
3460
3486
  margin: "12px 0 2px"
3461
3487
  });
3488
+ var limitExceededHintStyle = (color) => ({
3489
+ textAlign: "center",
3490
+ fontSize: "0.78rem",
3491
+ color,
3492
+ margin: "12px 0 2px",
3493
+ lineHeight: 1.5
3494
+ });
3462
3495
  var routeStyle = (color) => ({
3463
3496
  textAlign: "center",
3464
3497
  fontSize: "0.75rem",
@@ -5207,6 +5240,81 @@ function SwypePaymentInner({
5207
5240
  merchantAuthorization,
5208
5241
  transfer
5209
5242
  ]);
5243
+ const [increasingLimit, setIncreasingLimit] = useState(false);
5244
+ const handleIncreaseLimit = useCallback(async () => {
5245
+ const parsedAmount = depositAmount ?? 5;
5246
+ if (!sourceId) {
5247
+ setError("No account or provider selected.");
5248
+ return;
5249
+ }
5250
+ if (!activeCredentialId) {
5251
+ setError("Create a passkey on this device before continuing.");
5252
+ setStep("create-passkey");
5253
+ return;
5254
+ }
5255
+ setError(null);
5256
+ setIncreasingLimit(true);
5257
+ try {
5258
+ const token = await getAccessToken();
5259
+ if (!token) throw new Error("Not authenticated");
5260
+ let effectiveSourceType = sourceType;
5261
+ let effectiveSourceId = sourceId;
5262
+ if (effectiveSourceType === "accountId") {
5263
+ const acct = accounts.find((a) => a.id === effectiveSourceId);
5264
+ const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
5265
+ if (activeWallet) {
5266
+ effectiveSourceType = "walletId";
5267
+ effectiveSourceId = activeWallet.id;
5268
+ }
5269
+ }
5270
+ const t = await createTransfer(apiBaseUrl, token, {
5271
+ id: idempotencyKey,
5272
+ credentialId: activeCredentialId,
5273
+ merchantAuthorization,
5274
+ sourceType: effectiveSourceType,
5275
+ sourceId: effectiveSourceId,
5276
+ destination,
5277
+ amount: parsedAmount
5278
+ });
5279
+ setTransfer(t);
5280
+ if (t.authorizationSessions && t.authorizationSessions.length > 0) {
5281
+ const uri = t.authorizationSessions[0].uri;
5282
+ setMobileFlow(true);
5283
+ pollingTransferIdRef.current = t.id;
5284
+ mobileSetupFlowRef.current = true;
5285
+ handlingMobileReturnRef.current = false;
5286
+ polling.startPolling(t.id);
5287
+ setDeeplinkUri(uri);
5288
+ persistMobileFlowState({
5289
+ transferId: t.id,
5290
+ deeplinkUri: uri,
5291
+ providerId: selectedProviderId,
5292
+ isSetup: true
5293
+ });
5294
+ triggerDeeplink(uri);
5295
+ }
5296
+ } catch (err) {
5297
+ const msg = err instanceof Error ? err.message : "Failed to increase limit";
5298
+ setError(msg);
5299
+ onError?.(msg);
5300
+ } finally {
5301
+ setIncreasingLimit(false);
5302
+ }
5303
+ }, [
5304
+ depositAmount,
5305
+ sourceId,
5306
+ sourceType,
5307
+ activeCredentialId,
5308
+ apiBaseUrl,
5309
+ getAccessToken,
5310
+ accounts,
5311
+ polling,
5312
+ onError,
5313
+ idempotencyKey,
5314
+ merchantAuthorization,
5315
+ destination,
5316
+ selectedProviderId
5317
+ ]);
5210
5318
  const completePasskeyRegistration = useCallback(async (credentialId, publicKey) => {
5211
5319
  const token = await getAccessToken();
5212
5320
  if (!token) throw new Error("Not authenticated");
@@ -5468,7 +5576,9 @@ function SwypePaymentInner({
5468
5576
  onChangeSource: () => setStep("wallet-picker"),
5469
5577
  onSwitchWallet: () => setStep("wallet-picker"),
5470
5578
  onBack: onBack ?? (() => handleLogout()),
5471
- onLogout: handleLogout
5579
+ onLogout: handleLogout,
5580
+ onIncreaseLimit: handleIncreaseLimit,
5581
+ increasingLimit
5472
5582
  }
5473
5583
  );
5474
5584
  }