@swype-org/react-sdk 0.1.63 → 0.1.67

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
@@ -657,6 +657,24 @@ function normalizeSignature(sig) {
657
657
  );
658
658
  }
659
659
 
660
+ // src/transferPolling.ts
661
+ async function pollTransferTick(params) {
662
+ const fetchTransfer2 = params.fetchTransfer ?? fetchTransfer;
663
+ const token = await params.getAccessToken();
664
+ if (!token) {
665
+ return { kind: "retry" };
666
+ }
667
+ try {
668
+ const transfer = await fetchTransfer2(params.apiBaseUrl, token, params.transferId);
669
+ return { kind: "success", transfer };
670
+ } catch (err) {
671
+ return {
672
+ kind: "error",
673
+ message: err instanceof Error ? err.message : "Polling error"
674
+ };
675
+ }
676
+ }
677
+
660
678
  // src/passkey-delegation.ts
661
679
  var PasskeyIframeBlockedError = class extends Error {
662
680
  constructor(message = "Passkey creation is not supported in this browser context.") {
@@ -959,20 +977,22 @@ function useTransferPolling(intervalMs = 3e3) {
959
977
  }, []);
960
978
  const poll = react.useCallback(async () => {
961
979
  if (!transferIdRef.current) return;
962
- try {
963
- const token = await getAccessToken();
964
- if (!token) {
965
- setError("Could not get access token");
966
- stopPolling();
967
- return;
968
- }
969
- const t = await fetchTransfer(apiBaseUrl, token, transferIdRef.current);
970
- setTransfer(t);
971
- if (t.status === "COMPLETED" || t.status === "FAILED") {
972
- stopPolling();
973
- }
974
- } catch (err) {
975
- setError(err instanceof Error ? err.message : "Polling error");
980
+ const result = await pollTransferTick({
981
+ apiBaseUrl,
982
+ transferId: transferIdRef.current,
983
+ getAccessToken
984
+ });
985
+ if (result.kind === "retry") {
986
+ return;
987
+ }
988
+ if (result.kind === "error") {
989
+ setError(result.message);
990
+ stopPolling();
991
+ return;
992
+ }
993
+ setError(null);
994
+ setTransfer(result.transfer);
995
+ if (result.transfer.status === "COMPLETED" || result.transfer.status === "FAILED") {
976
996
  stopPolling();
977
997
  }
978
998
  }, [apiBaseUrl, getAccessToken, stopPolling]);
@@ -1583,6 +1603,35 @@ function isMobileUserAgent(userAgent) {
1583
1603
  function shouldUseWalletConnector(options) {
1584
1604
  return options.useWalletConnector ?? !isMobileUserAgent(options.userAgent);
1585
1605
  }
1606
+
1607
+ // src/mobileFlow.ts
1608
+ function hasActiveWallet(accounts) {
1609
+ return accounts.some((account) => account.wallets.some((wallet) => wallet.status === "ACTIVE"));
1610
+ }
1611
+ function resolvePostAuthStep(state) {
1612
+ if (!state.hasPasskey) {
1613
+ return { step: "create-passkey", clearPersistedFlow: false };
1614
+ }
1615
+ if (state.persistedMobileFlow) {
1616
+ return { step: "open-wallet", clearPersistedFlow: false };
1617
+ }
1618
+ if ((state.accounts.length === 0 || !hasActiveWallet(state.accounts)) && !state.connectingNewAccount) {
1619
+ return { step: "wallet-picker", clearPersistedFlow: false };
1620
+ }
1621
+ return { step: "deposit", clearPersistedFlow: false };
1622
+ }
1623
+ function resolveRestoredMobileFlow(transferStatus, isSetup) {
1624
+ if (transferStatus === "AUTHORIZED") {
1625
+ return isSetup ? { kind: "resume-setup-deposit", step: "deposit", clearPersistedFlow: true } : { kind: "resume-confirm-sign", step: "confirm-sign", clearPersistedFlow: true };
1626
+ }
1627
+ if (transferStatus === "COMPLETED") {
1628
+ return { kind: "resume-success", step: "success", clearPersistedFlow: true };
1629
+ }
1630
+ if (transferStatus === "FAILED") {
1631
+ return { kind: "resume-failed", step: "success", clearPersistedFlow: true };
1632
+ }
1633
+ return { kind: "resume-open-wallet", step: "open-wallet", clearPersistedFlow: false };
1634
+ }
1586
1635
  var FOOTER_CSS = `
1587
1636
  .swype-screen-footer {
1588
1637
  padding-bottom: max(24px, env(safe-area-inset-bottom, 24px));
@@ -1911,137 +1960,6 @@ var inputStyle = (tokens, filled) => ({
1911
1960
  caretColor: tokens.borderFocus,
1912
1961
  transition: "border-color 0.15s ease"
1913
1962
  });
1914
- function LimitSlider({
1915
- value,
1916
- min,
1917
- max,
1918
- step = 1,
1919
- onChange,
1920
- ticks,
1921
- disabled
1922
- }) {
1923
- const { tokens } = useSwypeConfig();
1924
- const pct = (value - min) / (max - min) * 100;
1925
- const handleChange = react.useCallback(
1926
- (e) => onChange(Number(e.target.value)),
1927
- [onChange]
1928
- );
1929
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "swype-slider-wrap", style: wrapperStyle, children: [
1930
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: trackContainerStyle, children: [
1931
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: trackBgStyle(tokens.border) }),
1932
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: trackFillStyle(tokens.accent, pct) }),
1933
- /* @__PURE__ */ jsxRuntime.jsx(
1934
- "input",
1935
- {
1936
- type: "range",
1937
- min,
1938
- max,
1939
- step,
1940
- value,
1941
- onChange: handleChange,
1942
- disabled,
1943
- style: rangeInputStyle
1944
- }
1945
- )
1946
- ] }),
1947
- ticks && ticks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: ticksStyle, children: ticks.map((tick, i) => {
1948
- const pctPos = (tick - min) / (max - min) * 100;
1949
- const isFirst = i === 0;
1950
- const isLast = i === ticks.length - 1;
1951
- const label = tick % 1 === 0 ? `$${tick}` : `$${tick.toFixed(2)}`;
1952
- return /* @__PURE__ */ jsxRuntime.jsx(
1953
- "span",
1954
- {
1955
- style: tickLabelStyle(tokens.textMuted, pctPos, isFirst, isLast),
1956
- children: label
1957
- },
1958
- tick
1959
- );
1960
- }) }),
1961
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: sliderThumbCss(tokens.accent) })
1962
- ] });
1963
- }
1964
- var wrapperStyle = { width: "100%" };
1965
- var trackContainerStyle = {
1966
- position: "relative",
1967
- height: 28,
1968
- display: "flex",
1969
- alignItems: "center"
1970
- };
1971
- var trackBgStyle = (color) => ({
1972
- position: "absolute",
1973
- left: 0,
1974
- right: 0,
1975
- height: 4,
1976
- borderRadius: 2,
1977
- background: color
1978
- });
1979
- var trackFillStyle = (color, pct) => ({
1980
- position: "absolute",
1981
- left: 0,
1982
- width: `${pct}%`,
1983
- height: 4,
1984
- borderRadius: 2,
1985
- background: color
1986
- });
1987
- var rangeInputStyle = {
1988
- position: "absolute",
1989
- left: 0,
1990
- right: 0,
1991
- width: "100%",
1992
- height: 28,
1993
- margin: 0,
1994
- cursor: "pointer",
1995
- zIndex: 2,
1996
- WebkitAppearance: "none",
1997
- appearance: "none",
1998
- background: "transparent"
1999
- };
2000
- var ticksStyle = {
2001
- position: "relative",
2002
- height: 18,
2003
- marginTop: 6
2004
- };
2005
- var tickLabelStyle = (color, pct, isFirst, isLast) => ({
2006
- position: "absolute",
2007
- left: `${pct}%`,
2008
- transform: isFirst ? "none" : isLast ? "translateX(-100%)" : "translateX(-50%)",
2009
- fontSize: "0.72rem",
2010
- fontWeight: 500,
2011
- color,
2012
- whiteSpace: "nowrap"
2013
- });
2014
- var sliderThumbCss = (accent) => `
2015
- .swype-slider-wrap input[type="range"]::-webkit-slider-runnable-track {
2016
- height: 4px;
2017
- background: transparent;
2018
- }
2019
- .swype-slider-wrap input[type="range"]::-webkit-slider-thumb {
2020
- -webkit-appearance: none;
2021
- width: 20px;
2022
- height: 20px;
2023
- border-radius: 50%;
2024
- background: ${accent};
2025
- border: 3px solid #fff;
2026
- box-shadow: 0 2px 6px rgba(0,0,0,0.15);
2027
- cursor: pointer;
2028
- margin-top: -8px;
2029
- }
2030
- .swype-slider-wrap input[type="range"]::-moz-range-track {
2031
- height: 4px;
2032
- background: transparent;
2033
- border: none;
2034
- }
2035
- .swype-slider-wrap input[type="range"]::-moz-range-thumb {
2036
- width: 14px;
2037
- height: 14px;
2038
- border-radius: 50%;
2039
- background: ${accent};
2040
- border: 3px solid #fff;
2041
- box-shadow: 0 2px 6px rgba(0,0,0,0.15);
2042
- cursor: pointer;
2043
- }
2044
- `;
2045
1963
 
2046
1964
  // src/assets/logos.ts
2047
1965
  function svgToDataUri(svg) {
@@ -3003,18 +2921,6 @@ var dividerTextStyle = (color) => ({
3003
2921
  });
3004
2922
  var DEFAULT_MAX = 500;
3005
2923
  var ABSOLUTE_MIN = 1;
3006
- function buildTicks(min, max) {
3007
- if (max <= min) return [min];
3008
- const range = max - min;
3009
- const candidates = [1, 2, 5, 10, 25, 50, 100, 250];
3010
- const step = candidates.find((s) => range / s <= 5) ?? Math.ceil(range / 4);
3011
- const ticks = [];
3012
- for (let v = min; v <= max; v += step) {
3013
- ticks.push(Math.round(v * 100) / 100);
3014
- }
3015
- if (ticks[ticks.length - 1] !== max) ticks.push(max);
3016
- return ticks;
3017
- }
3018
2924
  function SetupScreen({
3019
2925
  availableBalance,
3020
2926
  tokenCount,
@@ -3030,9 +2936,22 @@ function SetupScreen({
3030
2936
  const { tokens } = useSwypeConfig();
3031
2937
  const effectiveMax = Math.floor(Math.min(DEFAULT_MAX, availableBalance > 0 ? availableBalance : DEFAULT_MAX) * 100) / 100;
3032
2938
  const effectiveMin = Math.min(ABSOLUTE_MIN, effectiveMax);
3033
- const sliderStep = effectiveMax <= 10 ? 0.5 : effectiveMax <= 50 ? 1 : 5;
3034
- const ticks = buildTicks(effectiveMin, effectiveMax);
3035
- const [limit, setLimit] = react.useState(() => Math.min(100, effectiveMax));
2939
+ const [limit, setLimit] = react.useState(() => effectiveMax);
2940
+ const [editing, setEditing] = react.useState(false);
2941
+ const [inputValue, setInputValue] = react.useState("");
2942
+ const inputRef = react.useRef(null);
2943
+ const startEditing = react.useCallback(() => {
2944
+ setInputValue(limit.toFixed(2));
2945
+ setEditing(true);
2946
+ requestAnimationFrame(() => inputRef.current?.select());
2947
+ }, [limit]);
2948
+ const commitEdit = react.useCallback(() => {
2949
+ const parsed = parseFloat(inputValue);
2950
+ if (!isNaN(parsed)) {
2951
+ setLimit(Math.min(effectiveMax, Math.max(effectiveMin, Math.round(parsed * 100) / 100)));
2952
+ }
2953
+ setEditing(false);
2954
+ }, [inputValue, effectiveMax, effectiveMin]);
3036
2955
  return /* @__PURE__ */ jsxRuntime.jsxs(
3037
2956
  ScreenLayout,
3038
2957
  {
@@ -3088,19 +3007,33 @@ function SetupScreen({
3088
3007
  ] }),
3089
3008
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: limitSectionStyle, children: [
3090
3009
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: limitLabelStyle(tokens.textMuted), children: "Your One-Tap limit" }),
3091
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: limitValueStyle(tokens.text), children: [
3010
+ editing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: limitValueStyle(tokens.text), children: [
3092
3011
  "$",
3093
- limit.toFixed(2)
3094
- ] }),
3095
- /* @__PURE__ */ jsxRuntime.jsx(
3096
- LimitSlider,
3012
+ /* @__PURE__ */ jsxRuntime.jsx(
3013
+ "input",
3014
+ {
3015
+ ref: inputRef,
3016
+ type: "text",
3017
+ inputMode: "decimal",
3018
+ pattern: "[0-9]*",
3019
+ value: inputValue,
3020
+ onChange: (e) => setInputValue(e.target.value),
3021
+ onBlur: commitEdit,
3022
+ onKeyDown: (e) => {
3023
+ if (e.key === "Enter") commitEdit();
3024
+ },
3025
+ style: limitInputStyle(tokens.text)
3026
+ }
3027
+ )
3028
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
3029
+ "div",
3097
3030
  {
3098
- value: limit,
3099
- min: effectiveMin,
3100
- max: effectiveMax,
3101
- step: sliderStep,
3102
- ticks,
3103
- onChange: setLimit
3031
+ style: { ...limitValueStyle(tokens.text), cursor: "pointer" },
3032
+ onClick: startEditing,
3033
+ children: [
3034
+ "$",
3035
+ limit.toFixed(2)
3036
+ ]
3104
3037
  }
3105
3038
  )
3106
3039
  ] }),
@@ -3207,6 +3140,19 @@ var limitValueStyle = (color) => ({
3207
3140
  color,
3208
3141
  marginBottom: 12
3209
3142
  });
3143
+ var limitInputStyle = (color) => ({
3144
+ fontSize: "2.2rem",
3145
+ fontWeight: 700,
3146
+ color,
3147
+ background: "transparent",
3148
+ border: "none",
3149
+ borderBottom: "2px solid currentColor",
3150
+ outline: "none",
3151
+ textAlign: "center",
3152
+ width: "5ch",
3153
+ fontFamily: "inherit",
3154
+ padding: 0
3155
+ });
3210
3156
  var bannerWrapStyle = { marginBottom: 16 };
3211
3157
  var linkStyle = (color) => ({
3212
3158
  background: "transparent",
@@ -4112,6 +4058,8 @@ function OpenWalletScreen({
4112
4058
  walletName,
4113
4059
  deeplinkUri,
4114
4060
  loading,
4061
+ error,
4062
+ onRetryStatus,
4115
4063
  onLogout
4116
4064
  }) {
4117
4065
  const { tokens } = useSwypeConfig();
@@ -4132,12 +4080,14 @@ function OpenWalletScreen({
4132
4080
  return /* @__PURE__ */ jsxRuntime.jsxs(
4133
4081
  ScreenLayout,
4134
4082
  {
4135
- footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4083
+ footer: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: footerContentStyle, children: [
4084
+ error && /* @__PURE__ */ jsxRuntime.jsx(InfoBanner, { children: error }),
4136
4085
  !loading && /* @__PURE__ */ jsxRuntime.jsxs(PrimaryButton, { onClick: handleOpen, children: [
4137
4086
  "Open ",
4138
4087
  displayName
4139
4088
  ] }),
4140
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: hintStyle3(tokens.textMuted), children: loading ? "Preparing authorization..." : "If your wallet didn't open automatically, tap the button above" })
4089
+ error && onRetryStatus && /* @__PURE__ */ jsxRuntime.jsx(OutlineButton, { onClick: onRetryStatus, children: "Retry status check" }),
4090
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: hintStyle3(tokens.textMuted), children: loading ? "Preparing authorization..." : error ? "Retry the status check after returning to the browser, or reopen your wallet if needed." : "If your wallet didn't open automatically, tap the button above" })
4141
4091
  ] }),
4142
4092
  children: [
4143
4093
  /* @__PURE__ */ jsxRuntime.jsx(ScreenHeader, { right: /* @__PURE__ */ jsxRuntime.jsx(SettingsMenu, { onLogout }) }),
@@ -4163,6 +4113,11 @@ var contentStyle6 = {
4163
4113
  textAlign: "center",
4164
4114
  padding: "0 24px"
4165
4115
  };
4116
+ var footerContentStyle = {
4117
+ display: "flex",
4118
+ flexDirection: "column",
4119
+ gap: 12
4120
+ };
4166
4121
  var logoStyle = {
4167
4122
  width: 56,
4168
4123
  height: 56,
@@ -4541,6 +4496,55 @@ function SwypePaymentInner({
4541
4496
  setConnectingNewAccount(false);
4542
4497
  }
4543
4498
  }, [getAccessToken, activeCredentialId, apiBaseUrl, depositAmount]);
4499
+ const enterPersistedMobileFlow = react.useCallback((persisted, errorMessage) => {
4500
+ setMobileFlow(true);
4501
+ setDeeplinkUri(persisted.deeplinkUri);
4502
+ setSelectedProviderId(persisted.providerId);
4503
+ pollingTransferIdRef.current = persisted.transferId;
4504
+ mobileSetupFlowRef.current = persisted.isSetup;
4505
+ setError(errorMessage ?? null);
4506
+ setStep("open-wallet");
4507
+ polling.startPolling(persisted.transferId);
4508
+ }, [polling]);
4509
+ const handleAuthorizedMobileReturn = react.useCallback(async (authorizedTransfer, isSetup) => {
4510
+ setTransfer(authorizedTransfer);
4511
+ polling.stopPolling();
4512
+ if (isSetup) {
4513
+ mobileSetupFlowRef.current = false;
4514
+ clearMobileFlowState();
4515
+ try {
4516
+ await reloadAccounts();
4517
+ setError(null);
4518
+ setDeeplinkUri(null);
4519
+ setMobileFlow(false);
4520
+ setStep("deposit");
4521
+ } catch (err) {
4522
+ setError(
4523
+ err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
4524
+ );
4525
+ setStep("open-wallet");
4526
+ }
4527
+ return;
4528
+ }
4529
+ mobileSetupFlowRef.current = false;
4530
+ clearMobileFlowState();
4531
+ setError(null);
4532
+ setDeeplinkUri(null);
4533
+ setMobileFlow(false);
4534
+ setStep("confirm-sign");
4535
+ }, [polling.stopPolling, reloadAccounts]);
4536
+ const handleRetryMobileStatus = react.useCallback(() => {
4537
+ setError(null);
4538
+ const currentTransfer = polling.transfer ?? transfer;
4539
+ if (currentTransfer?.status === "AUTHORIZED") {
4540
+ void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
4541
+ return;
4542
+ }
4543
+ const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
4544
+ if (transferIdToResume) {
4545
+ polling.startPolling(transferIdToResume);
4546
+ }
4547
+ }, [handleAuthorizedMobileReturn, polling, transfer]);
4544
4548
  react.useEffect(() => {
4545
4549
  if (depositAmount != null) {
4546
4550
  setAmount(depositAmount.toString());
@@ -4626,19 +4630,70 @@ function SwypePaymentInner({
4626
4630
  let cancelled = false;
4627
4631
  setError(null);
4628
4632
  resetHeadlessLogin();
4629
- const restoreOrDeposit = () => {
4633
+ const restoreOrDeposit = async (credId, token) => {
4630
4634
  const persisted = loadMobileFlowState();
4631
- if (persisted) {
4632
- setMobileFlow(true);
4633
- setDeeplinkUri(persisted.deeplinkUri);
4634
- setSelectedProviderId(persisted.providerId);
4635
- pollingTransferIdRef.current = persisted.transferId;
4636
- mobileSetupFlowRef.current = persisted.isSetup;
4637
- setStep("open-wallet");
4638
- polling.startPolling(persisted.transferId);
4639
- } else {
4640
- setStep("deposit");
4635
+ let accts = [];
4636
+ try {
4637
+ accts = await fetchAccounts(apiBaseUrl, token, credId);
4638
+ if (cancelled) return;
4639
+ } catch {
4640
+ }
4641
+ const resolved = resolvePostAuthStep({
4642
+ hasPasskey: true,
4643
+ accounts: accts,
4644
+ persistedMobileFlow: persisted,
4645
+ connectingNewAccount: false
4646
+ });
4647
+ if (resolved.clearPersistedFlow) {
4648
+ clearMobileFlowState();
4649
+ }
4650
+ if (resolved.step === "open-wallet" && persisted) {
4651
+ try {
4652
+ const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
4653
+ if (cancelled) return;
4654
+ const mobileResolution = resolveRestoredMobileFlow(
4655
+ existingTransfer.status,
4656
+ persisted.isSetup
4657
+ );
4658
+ if (mobileResolution.kind === "resume-setup-deposit") {
4659
+ await handleAuthorizedMobileReturn(existingTransfer, true);
4660
+ return;
4661
+ }
4662
+ if (mobileResolution.kind === "resume-confirm-sign") {
4663
+ await handleAuthorizedMobileReturn(existingTransfer, false);
4664
+ return;
4665
+ }
4666
+ if (mobileResolution.kind === "resume-success") {
4667
+ clearMobileFlowState();
4668
+ setMobileFlow(false);
4669
+ setDeeplinkUri(null);
4670
+ setTransfer(existingTransfer);
4671
+ setError(null);
4672
+ setStep("success");
4673
+ onComplete?.(existingTransfer);
4674
+ return;
4675
+ }
4676
+ if (mobileResolution.kind === "resume-failed") {
4677
+ clearMobileFlowState();
4678
+ setMobileFlow(false);
4679
+ setDeeplinkUri(null);
4680
+ setTransfer(existingTransfer);
4681
+ setError("Transfer failed.");
4682
+ setStep("success");
4683
+ return;
4684
+ }
4685
+ } catch (err) {
4686
+ if (cancelled) return;
4687
+ enterPersistedMobileFlow(
4688
+ persisted,
4689
+ err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
4690
+ );
4691
+ return;
4692
+ }
4693
+ enterPersistedMobileFlow(persisted);
4694
+ return;
4641
4695
  }
4696
+ setStep(resolved.step);
4642
4697
  };
4643
4698
  const checkPasskey = async () => {
4644
4699
  try {
@@ -4655,7 +4710,7 @@ function SwypePaymentInner({
4655
4710
  return;
4656
4711
  }
4657
4712
  if (activeCredentialId && allPasskeys.some((p) => p.credentialId === activeCredentialId)) {
4658
- restoreOrDeposit();
4713
+ await restoreOrDeposit(activeCredentialId, token);
4659
4714
  return;
4660
4715
  }
4661
4716
  if (cancelled) return;
@@ -4665,7 +4720,7 @@ function SwypePaymentInner({
4665
4720
  if (matched) {
4666
4721
  setActiveCredentialId(matched);
4667
4722
  window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
4668
- restoreOrDeposit();
4723
+ await restoreOrDeposit(matched, token);
4669
4724
  return;
4670
4725
  }
4671
4726
  setStep("create-passkey");
@@ -4677,7 +4732,18 @@ function SwypePaymentInner({
4677
4732
  return () => {
4678
4733
  cancelled = true;
4679
4734
  };
4680
- }, [ready, authenticated, step, apiBaseUrl, getAccessToken, activeCredentialId, resetHeadlessLogin]);
4735
+ }, [
4736
+ ready,
4737
+ authenticated,
4738
+ step,
4739
+ apiBaseUrl,
4740
+ getAccessToken,
4741
+ activeCredentialId,
4742
+ resetHeadlessLogin,
4743
+ enterPersistedMobileFlow,
4744
+ handleAuthorizedMobileReturn,
4745
+ onComplete
4746
+ ]);
4681
4747
  const loadingDataRef = react.useRef(false);
4682
4748
  react.useEffect(() => {
4683
4749
  if (!authenticated) return;
@@ -4709,11 +4775,22 @@ function SwypePaymentInner({
4709
4775
  } else if (prov.length > 0 && !connectingNewAccount) {
4710
4776
  setSelectedProviderId(prov[0].id);
4711
4777
  }
4712
- const hasActiveWallet = accts.some(
4713
- (a) => a.wallets.some((w) => w.status === "ACTIVE")
4714
- );
4715
- if ((accts.length === 0 || !hasActiveWallet) && step === "deposit" && !connectingNewAccount) {
4716
- setStep("wallet-picker");
4778
+ const persisted = loadMobileFlowState();
4779
+ const resolved = resolvePostAuthStep({
4780
+ hasPasskey: !!activeCredentialId,
4781
+ accounts: accts,
4782
+ persistedMobileFlow: persisted,
4783
+ mobileSetupInProgress: mobileSetupFlowRef.current,
4784
+ connectingNewAccount
4785
+ });
4786
+ if (resolved.clearPersistedFlow) {
4787
+ clearMobileFlowState();
4788
+ setMobileFlow(false);
4789
+ setDeeplinkUri(null);
4790
+ }
4791
+ const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
4792
+ if (correctableSteps.includes(step)) {
4793
+ setStep(resolved.step);
4717
4794
  }
4718
4795
  } catch (err) {
4719
4796
  if (!cancelled) {
@@ -4776,22 +4853,8 @@ function SwypePaymentInner({
4776
4853
  if (!mobileFlow) return;
4777
4854
  const polledTransfer = polling.transfer;
4778
4855
  if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
4779
- if (mobileSetupFlowRef.current) {
4780
- mobileSetupFlowRef.current = false;
4781
- clearMobileFlowState();
4782
- setDeeplinkUri(null);
4783
- polling.stopPolling();
4784
- setTransfer(polledTransfer);
4785
- reloadAccounts().catch(() => {
4786
- }).then(() => {
4787
- setMobileFlow(false);
4788
- setStep("deposit");
4789
- });
4790
- return;
4791
- }
4792
- setTransfer(polledTransfer);
4793
- setStep("confirm-sign");
4794
- }, [mobileFlow, polling.transfer, polling.stopPolling, transferSigning, onError, reloadAccounts]);
4856
+ void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
4857
+ }, [mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
4795
4858
  react.useEffect(() => {
4796
4859
  if (!mobileFlow) return;
4797
4860
  const transferIdToResume = pollingTransferIdRef.current ?? transfer?.id;
@@ -5024,15 +5087,18 @@ function SwypePaymentInner({
5024
5087
  setActiveCredentialId(credentialId);
5025
5088
  window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
5026
5089
  setPasskeyPopupNeeded(false);
5027
- const hasActiveWallet = accounts.some(
5028
- (a) => a.wallets.some((w) => w.status === "ACTIVE")
5029
- );
5030
- if (accounts.length === 0 || !hasActiveWallet) {
5031
- setStep("wallet-picker");
5032
- } else {
5033
- setStep("deposit");
5090
+ const resolved = resolvePostAuthStep({
5091
+ hasPasskey: true,
5092
+ accounts,
5093
+ persistedMobileFlow: loadMobileFlowState(),
5094
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5095
+ connectingNewAccount
5096
+ });
5097
+ if (resolved.clearPersistedFlow) {
5098
+ clearMobileFlowState();
5034
5099
  }
5035
- }, [getAccessToken, apiBaseUrl, accounts]);
5100
+ setStep(resolved.step);
5101
+ }, [getAccessToken, apiBaseUrl, accounts, connectingNewAccount]);
5036
5102
  const handleRegisterPasskey = react.useCallback(async () => {
5037
5103
  setRegisteringPasskey(true);
5038
5104
  setError(null);
@@ -5230,6 +5296,8 @@ function SwypePaymentInner({
5230
5296
  walletName: providerName,
5231
5297
  deeplinkUri: deeplinkUri ?? "",
5232
5298
  loading: creatingTransfer || !deeplinkUri,
5299
+ error: error || polling.error,
5300
+ onRetryStatus: handleRetryMobileStatus,
5233
5301
  onLogout: handleLogout
5234
5302
  }
5235
5303
  );