@swype-org/react-sdk 0.1.87 → 0.1.89
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 +1421 -1158
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -28
- package/dist/index.d.ts +12 -28
- package/dist/index.js +1422 -1159
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -728,10 +728,10 @@ function isSafari() {
|
|
|
728
728
|
var POPUP_RESULT_TIMEOUT_MS = 12e4;
|
|
729
729
|
var POPUP_CLOSED_POLL_MS = 500;
|
|
730
730
|
var POPUP_CLOSED_GRACE_MS = 1e3;
|
|
731
|
-
function createPasskeyViaPopup(options
|
|
731
|
+
function createPasskeyViaPopup(options) {
|
|
732
732
|
return new Promise((resolve, reject) => {
|
|
733
|
-
const
|
|
734
|
-
const payload = { ...options,
|
|
733
|
+
const verificationToken = crypto.randomUUID();
|
|
734
|
+
const payload = { ...options, verificationToken };
|
|
735
735
|
const encoded = btoa(JSON.stringify(payload));
|
|
736
736
|
const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
|
|
737
737
|
const popup = window.open(popupUrl, "swype-passkey");
|
|
@@ -740,22 +740,21 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
|
|
|
740
740
|
return;
|
|
741
741
|
}
|
|
742
742
|
let settled = false;
|
|
743
|
-
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
|
|
744
743
|
const timer = setTimeout(() => {
|
|
745
744
|
cleanup();
|
|
746
745
|
reject(new Error("Passkey creation timed out. Please try again."));
|
|
747
746
|
}, POPUP_RESULT_TIMEOUT_MS);
|
|
748
|
-
let closedGraceTimer = null;
|
|
749
747
|
const closedPoll = setInterval(() => {
|
|
750
748
|
if (popup.closed) {
|
|
751
749
|
clearInterval(closedPoll);
|
|
752
|
-
|
|
750
|
+
setTimeout(() => {
|
|
753
751
|
if (!settled) {
|
|
752
|
+
settled = true;
|
|
754
753
|
cleanup();
|
|
755
|
-
|
|
754
|
+
checkServerForPasskeyByToken(
|
|
756
755
|
options.authToken,
|
|
757
756
|
options.apiBaseUrl,
|
|
758
|
-
|
|
757
|
+
verificationToken
|
|
759
758
|
).then((result) => {
|
|
760
759
|
if (result) {
|
|
761
760
|
resolve(result);
|
|
@@ -769,45 +768,18 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
|
|
|
769
768
|
}, POPUP_CLOSED_GRACE_MS);
|
|
770
769
|
}
|
|
771
770
|
}, POPUP_CLOSED_POLL_MS);
|
|
772
|
-
function handleResult(data) {
|
|
773
|
-
if (settled) return;
|
|
774
|
-
if (!data || typeof data !== "object") return;
|
|
775
|
-
if (data.type !== "swype:passkey-popup-result") return;
|
|
776
|
-
settled = true;
|
|
777
|
-
cleanup();
|
|
778
|
-
if (data.error) {
|
|
779
|
-
reject(new Error(data.error));
|
|
780
|
-
} else if (data.result) {
|
|
781
|
-
resolve(data.result);
|
|
782
|
-
} else {
|
|
783
|
-
reject(new Error("Invalid passkey popup response."));
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if (channel) {
|
|
787
|
-
channel.onmessage = (event) => handleResult(event.data);
|
|
788
|
-
}
|
|
789
|
-
const postMessageHandler = (event) => {
|
|
790
|
-
if (event.source !== popup) return;
|
|
791
|
-
handleResult(event.data);
|
|
792
|
-
};
|
|
793
|
-
window.addEventListener("message", postMessageHandler);
|
|
794
771
|
function cleanup() {
|
|
795
772
|
clearTimeout(timer);
|
|
796
773
|
clearInterval(closedPoll);
|
|
797
|
-
if (closedGraceTimer) clearTimeout(closedGraceTimer);
|
|
798
|
-
window.removeEventListener("message", postMessageHandler);
|
|
799
|
-
channel?.close();
|
|
800
774
|
}
|
|
801
775
|
});
|
|
802
776
|
}
|
|
803
777
|
var VERIFY_POPUP_TIMEOUT_MS = 6e4;
|
|
804
778
|
function findDevicePasskeyViaPopup(options) {
|
|
805
779
|
return new Promise((resolve, reject) => {
|
|
806
|
-
const channelId = `swype-pv-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
807
780
|
const verificationToken = crypto.randomUUID();
|
|
808
781
|
const payload = {
|
|
809
782
|
...options,
|
|
810
|
-
channelId,
|
|
811
783
|
verificationToken
|
|
812
784
|
};
|
|
813
785
|
const encoded = btoa(JSON.stringify(payload));
|
|
@@ -818,7 +790,6 @@ function findDevicePasskeyViaPopup(options) {
|
|
|
818
790
|
return;
|
|
819
791
|
}
|
|
820
792
|
let settled = false;
|
|
821
|
-
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
|
|
822
793
|
const timer = setTimeout(() => {
|
|
823
794
|
cleanup();
|
|
824
795
|
resolve(null);
|
|
@@ -828,13 +799,14 @@ function findDevicePasskeyViaPopup(options) {
|
|
|
828
799
|
clearInterval(closedPoll);
|
|
829
800
|
setTimeout(() => {
|
|
830
801
|
if (!settled) {
|
|
802
|
+
settled = true;
|
|
831
803
|
cleanup();
|
|
832
|
-
|
|
804
|
+
checkServerForPasskeyByToken(
|
|
833
805
|
options.authToken,
|
|
834
806
|
options.apiBaseUrl,
|
|
835
807
|
verificationToken
|
|
836
|
-
).then((
|
|
837
|
-
resolve(credentialId);
|
|
808
|
+
).then((result) => {
|
|
809
|
+
resolve(result?.credentialId ?? null);
|
|
838
810
|
}).catch(() => {
|
|
839
811
|
resolve(null);
|
|
840
812
|
});
|
|
@@ -842,38 +814,13 @@ function findDevicePasskeyViaPopup(options) {
|
|
|
842
814
|
}, POPUP_CLOSED_GRACE_MS);
|
|
843
815
|
}
|
|
844
816
|
}, POPUP_CLOSED_POLL_MS);
|
|
845
|
-
function handleResult(data) {
|
|
846
|
-
if (settled) return;
|
|
847
|
-
if (!data || typeof data !== "object") return;
|
|
848
|
-
if (data.type !== "swype:passkey-verify-result") return;
|
|
849
|
-
settled = true;
|
|
850
|
-
cleanup();
|
|
851
|
-
if (data.error) {
|
|
852
|
-
resolve(null);
|
|
853
|
-
} else if (data.result && typeof data.result === "object") {
|
|
854
|
-
const result = data.result;
|
|
855
|
-
resolve(result.credentialId ?? null);
|
|
856
|
-
} else {
|
|
857
|
-
resolve(null);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (channel) {
|
|
861
|
-
channel.onmessage = (event) => handleResult(event.data);
|
|
862
|
-
}
|
|
863
|
-
const postMessageHandler = (event) => {
|
|
864
|
-
if (event.source !== popup) return;
|
|
865
|
-
handleResult(event.data);
|
|
866
|
-
};
|
|
867
|
-
window.addEventListener("message", postMessageHandler);
|
|
868
817
|
function cleanup() {
|
|
869
818
|
clearTimeout(timer);
|
|
870
819
|
clearInterval(closedPoll);
|
|
871
|
-
window.removeEventListener("message", postMessageHandler);
|
|
872
|
-
channel?.close();
|
|
873
820
|
}
|
|
874
821
|
});
|
|
875
822
|
}
|
|
876
|
-
async function
|
|
823
|
+
async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
|
|
877
824
|
if (!authToken || !apiBaseUrl) return null;
|
|
878
825
|
const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
|
|
879
826
|
headers: { Authorization: `Bearer ${authToken}` }
|
|
@@ -882,19 +829,7 @@ async function checkServerForVerifiedPasskey(authToken, apiBaseUrl, verification
|
|
|
882
829
|
const body = await res.json();
|
|
883
830
|
const passkeys = body.config.passkeys ?? [];
|
|
884
831
|
const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
|
|
885
|
-
return matched
|
|
886
|
-
}
|
|
887
|
-
async function checkServerForNewPasskey(authToken, apiBaseUrl, existingCredentialIds) {
|
|
888
|
-
if (!authToken || !apiBaseUrl) return null;
|
|
889
|
-
const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
|
|
890
|
-
headers: { Authorization: `Bearer ${authToken}` }
|
|
891
|
-
});
|
|
892
|
-
if (!res.ok) return null;
|
|
893
|
-
const body = await res.json();
|
|
894
|
-
const passkeys = body.config.passkeys ?? [];
|
|
895
|
-
const existingSet = new Set(existingCredentialIds);
|
|
896
|
-
const newPasskey = passkeys.find((p) => !existingSet.has(p.credentialId));
|
|
897
|
-
return newPasskey ?? null;
|
|
832
|
+
return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
|
|
898
833
|
}
|
|
899
834
|
|
|
900
835
|
// src/hooks.ts
|
|
@@ -1648,43 +1583,6 @@ function useTransferSigning(pollIntervalMs = 2e3, options) {
|
|
|
1648
1583
|
);
|
|
1649
1584
|
return { signing, signPayload, error, signTransfer: signTransfer2 };
|
|
1650
1585
|
}
|
|
1651
|
-
function Spinner({ size = 40, label }) {
|
|
1652
|
-
const { tokens } = useSwypeConfig();
|
|
1653
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1654
|
-
"div",
|
|
1655
|
-
{
|
|
1656
|
-
style: {
|
|
1657
|
-
display: "flex",
|
|
1658
|
-
flexDirection: "column",
|
|
1659
|
-
alignItems: "center",
|
|
1660
|
-
gap: "12px"
|
|
1661
|
-
},
|
|
1662
|
-
children: [
|
|
1663
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1664
|
-
"div",
|
|
1665
|
-
{
|
|
1666
|
-
style: {
|
|
1667
|
-
width: size,
|
|
1668
|
-
height: size,
|
|
1669
|
-
border: `4px solid ${tokens.bgInput}`,
|
|
1670
|
-
borderTopColor: tokens.accent,
|
|
1671
|
-
borderRightColor: tokens.accent + "66",
|
|
1672
|
-
borderRadius: "50%",
|
|
1673
|
-
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.1)",
|
|
1674
|
-
animation: "swype-spin 0.9s linear infinite"
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
),
|
|
1678
|
-
label && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: tokens.textSecondary, fontSize: "0.875rem", margin: 0 }, children: label }),
|
|
1679
|
-
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `
|
|
1680
|
-
@keyframes swype-spin {
|
|
1681
|
-
to { transform: rotate(360deg); }
|
|
1682
|
-
}
|
|
1683
|
-
` })
|
|
1684
|
-
]
|
|
1685
|
-
}
|
|
1686
|
-
);
|
|
1687
|
-
}
|
|
1688
1586
|
|
|
1689
1587
|
// src/auth.ts
|
|
1690
1588
|
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -1846,6 +1744,426 @@ function resolveDataLoadAction({
|
|
|
1846
1744
|
}
|
|
1847
1745
|
return "load";
|
|
1848
1746
|
}
|
|
1747
|
+
|
|
1748
|
+
// src/paymentHelpers.ts
|
|
1749
|
+
var ACTIVE_CREDENTIAL_STORAGE_KEY = "swype_active_credential_id";
|
|
1750
|
+
var MOBILE_FLOW_STORAGE_KEY = "swype_mobile_flow";
|
|
1751
|
+
var MIN_SEND_AMOUNT_USD = 0.25;
|
|
1752
|
+
function persistMobileFlowState(data) {
|
|
1753
|
+
try {
|
|
1754
|
+
sessionStorage.setItem(MOBILE_FLOW_STORAGE_KEY, JSON.stringify(data));
|
|
1755
|
+
} catch {
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function loadMobileFlowState() {
|
|
1759
|
+
try {
|
|
1760
|
+
const raw = sessionStorage.getItem(MOBILE_FLOW_STORAGE_KEY);
|
|
1761
|
+
if (!raw) return null;
|
|
1762
|
+
return JSON.parse(raw);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
function clearMobileFlowState() {
|
|
1768
|
+
try {
|
|
1769
|
+
sessionStorage.removeItem(MOBILE_FLOW_STORAGE_KEY);
|
|
1770
|
+
} catch {
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function computeSmartDefaults(accts, transferAmount) {
|
|
1774
|
+
if (accts.length === 0) return null;
|
|
1775
|
+
for (const acct of accts) {
|
|
1776
|
+
for (const wallet of acct.wallets) {
|
|
1777
|
+
if (wallet.status === "ACTIVE") {
|
|
1778
|
+
const bestSource = wallet.sources.find(
|
|
1779
|
+
(s) => s.balance.available.amount >= transferAmount
|
|
1780
|
+
);
|
|
1781
|
+
if (bestSource) {
|
|
1782
|
+
return { accountId: acct.id, walletId: wallet.id };
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
let bestAccount = null;
|
|
1788
|
+
let bestWallet = null;
|
|
1789
|
+
let bestBalance = -1;
|
|
1790
|
+
let bestIsActive = false;
|
|
1791
|
+
for (const acct of accts) {
|
|
1792
|
+
for (const wallet of acct.wallets) {
|
|
1793
|
+
const walletBal = wallet.balance.available.amount;
|
|
1794
|
+
const isActive = wallet.status === "ACTIVE";
|
|
1795
|
+
if (walletBal > bestBalance || walletBal === bestBalance && isActive && !bestIsActive) {
|
|
1796
|
+
bestBalance = walletBal;
|
|
1797
|
+
bestAccount = acct;
|
|
1798
|
+
bestWallet = wallet;
|
|
1799
|
+
bestIsActive = isActive;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (bestAccount) {
|
|
1804
|
+
return {
|
|
1805
|
+
accountId: bestAccount.id,
|
|
1806
|
+
walletId: bestWallet?.id ?? null
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return { accountId: accts[0].id, walletId: null };
|
|
1810
|
+
}
|
|
1811
|
+
function parseRawBalance(rawBalance, decimals) {
|
|
1812
|
+
const parsed = Number(rawBalance);
|
|
1813
|
+
if (!Number.isFinite(parsed)) return 0;
|
|
1814
|
+
return parsed / 10 ** decimals;
|
|
1815
|
+
}
|
|
1816
|
+
function buildSelectSourceChoices(options) {
|
|
1817
|
+
const chainChoices = [];
|
|
1818
|
+
const chainIndexByName = /* @__PURE__ */ new Map();
|
|
1819
|
+
for (const option of options) {
|
|
1820
|
+
const { chainName, tokenSymbol } = option;
|
|
1821
|
+
const balance = parseRawBalance(option.rawBalance, option.decimals);
|
|
1822
|
+
let chainChoice;
|
|
1823
|
+
const existingIdx = chainIndexByName.get(chainName);
|
|
1824
|
+
if (existingIdx === void 0) {
|
|
1825
|
+
chainChoice = { chainName, balance: 0, tokens: [] };
|
|
1826
|
+
chainIndexByName.set(chainName, chainChoices.length);
|
|
1827
|
+
chainChoices.push(chainChoice);
|
|
1828
|
+
} else {
|
|
1829
|
+
chainChoice = chainChoices[existingIdx];
|
|
1830
|
+
}
|
|
1831
|
+
chainChoice.balance += balance;
|
|
1832
|
+
const existing = chainChoice.tokens.find((t) => t.tokenSymbol === tokenSymbol);
|
|
1833
|
+
if (existing) {
|
|
1834
|
+
existing.balance += balance;
|
|
1835
|
+
} else {
|
|
1836
|
+
chainChoice.tokens.push({ tokenSymbol, balance });
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return chainChoices;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/paymentReducer.ts
|
|
1843
|
+
function deriveSourceTypeAndId(state) {
|
|
1844
|
+
if (state.connectingNewAccount) {
|
|
1845
|
+
return { sourceType: "providerId", sourceId: state.selectedProviderId ?? "" };
|
|
1846
|
+
}
|
|
1847
|
+
if (state.selectedWalletId) {
|
|
1848
|
+
return { sourceType: "walletId", sourceId: state.selectedWalletId };
|
|
1849
|
+
}
|
|
1850
|
+
if (state.selectedAccountId) {
|
|
1851
|
+
return { sourceType: "accountId", sourceId: state.selectedAccountId };
|
|
1852
|
+
}
|
|
1853
|
+
return { sourceType: "providerId", sourceId: state.selectedProviderId ?? "" };
|
|
1854
|
+
}
|
|
1855
|
+
function createInitialState(config) {
|
|
1856
|
+
return {
|
|
1857
|
+
step: "login",
|
|
1858
|
+
error: null,
|
|
1859
|
+
providers: [],
|
|
1860
|
+
accounts: [],
|
|
1861
|
+
chains: [],
|
|
1862
|
+
loadingData: false,
|
|
1863
|
+
selectedAccountId: null,
|
|
1864
|
+
selectedWalletId: null,
|
|
1865
|
+
selectedProviderId: null,
|
|
1866
|
+
connectingNewAccount: false,
|
|
1867
|
+
amount: config.depositAmount != null ? config.depositAmount.toString() : "",
|
|
1868
|
+
transfer: null,
|
|
1869
|
+
creatingTransfer: false,
|
|
1870
|
+
registeringPasskey: false,
|
|
1871
|
+
verifyingPasskeyPopup: false,
|
|
1872
|
+
passkeyPopupNeeded: config.passkeyPopupNeeded,
|
|
1873
|
+
activeCredentialId: config.activeCredentialId,
|
|
1874
|
+
knownCredentialIds: [],
|
|
1875
|
+
verificationTarget: null,
|
|
1876
|
+
oneTapLimit: 100,
|
|
1877
|
+
mobileFlow: false,
|
|
1878
|
+
deeplinkUri: null,
|
|
1879
|
+
increasingLimit: false
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function paymentReducer(state, action) {
|
|
1883
|
+
switch (action.type) {
|
|
1884
|
+
// ── Auth ──────────────────────────────────────────────────────
|
|
1885
|
+
case "CODE_SENT":
|
|
1886
|
+
return {
|
|
1887
|
+
...state,
|
|
1888
|
+
verificationTarget: action.target,
|
|
1889
|
+
error: null,
|
|
1890
|
+
step: "otp-verify"
|
|
1891
|
+
};
|
|
1892
|
+
case "BACK_TO_LOGIN":
|
|
1893
|
+
return {
|
|
1894
|
+
...state,
|
|
1895
|
+
verificationTarget: null,
|
|
1896
|
+
error: null,
|
|
1897
|
+
step: "login"
|
|
1898
|
+
};
|
|
1899
|
+
// ── Passkey ──────────────────────────────────────────────────
|
|
1900
|
+
case "PASSKEY_CONFIG_LOADED":
|
|
1901
|
+
return {
|
|
1902
|
+
...state,
|
|
1903
|
+
knownCredentialIds: action.knownIds,
|
|
1904
|
+
oneTapLimit: action.oneTapLimit ?? state.oneTapLimit
|
|
1905
|
+
};
|
|
1906
|
+
case "PASSKEY_ACTIVATED":
|
|
1907
|
+
return {
|
|
1908
|
+
...state,
|
|
1909
|
+
activeCredentialId: action.credentialId,
|
|
1910
|
+
passkeyPopupNeeded: false
|
|
1911
|
+
};
|
|
1912
|
+
case "SET_PASSKEY_POPUP_NEEDED":
|
|
1913
|
+
return { ...state, passkeyPopupNeeded: action.needed };
|
|
1914
|
+
case "SET_REGISTERING_PASSKEY":
|
|
1915
|
+
return { ...state, registeringPasskey: action.value };
|
|
1916
|
+
case "SET_VERIFYING_PASSKEY":
|
|
1917
|
+
return { ...state, verifyingPasskeyPopup: action.value };
|
|
1918
|
+
// ── Data loading ─────────────────────────────────────────────
|
|
1919
|
+
case "DATA_LOAD_START":
|
|
1920
|
+
return { ...state, loadingData: true, error: null };
|
|
1921
|
+
case "DATA_LOADED": {
|
|
1922
|
+
const next = {
|
|
1923
|
+
...state,
|
|
1924
|
+
providers: action.providers,
|
|
1925
|
+
accounts: action.accounts,
|
|
1926
|
+
chains: action.chains
|
|
1927
|
+
};
|
|
1928
|
+
if (action.defaults) {
|
|
1929
|
+
next.selectedAccountId = action.defaults.accountId;
|
|
1930
|
+
next.selectedWalletId = action.defaults.walletId;
|
|
1931
|
+
} else if (action.fallbackProviderId && !state.connectingNewAccount) {
|
|
1932
|
+
next.selectedProviderId = action.fallbackProviderId;
|
|
1933
|
+
}
|
|
1934
|
+
if (action.clearMobileState) {
|
|
1935
|
+
next.mobileFlow = false;
|
|
1936
|
+
next.deeplinkUri = null;
|
|
1937
|
+
}
|
|
1938
|
+
if (action.resolvedStep !== void 0) {
|
|
1939
|
+
next.step = action.resolvedStep;
|
|
1940
|
+
}
|
|
1941
|
+
return next;
|
|
1942
|
+
}
|
|
1943
|
+
case "DATA_LOAD_END":
|
|
1944
|
+
return { ...state, loadingData: false };
|
|
1945
|
+
case "ACCOUNTS_RELOADED": {
|
|
1946
|
+
const next = {
|
|
1947
|
+
...state,
|
|
1948
|
+
accounts: action.accounts,
|
|
1949
|
+
providers: action.providers
|
|
1950
|
+
};
|
|
1951
|
+
if (action.defaults) {
|
|
1952
|
+
next.selectedAccountId = action.defaults.accountId;
|
|
1953
|
+
next.selectedWalletId = action.defaults.walletId;
|
|
1954
|
+
next.connectingNewAccount = false;
|
|
1955
|
+
}
|
|
1956
|
+
return next;
|
|
1957
|
+
}
|
|
1958
|
+
// ── Source selection ──────────────────────────────────────────
|
|
1959
|
+
case "SELECT_PROVIDER":
|
|
1960
|
+
return {
|
|
1961
|
+
...state,
|
|
1962
|
+
selectedProviderId: action.providerId,
|
|
1963
|
+
selectedAccountId: null,
|
|
1964
|
+
connectingNewAccount: true
|
|
1965
|
+
};
|
|
1966
|
+
case "SELECT_ACCOUNT":
|
|
1967
|
+
return {
|
|
1968
|
+
...state,
|
|
1969
|
+
selectedAccountId: action.accountId,
|
|
1970
|
+
selectedWalletId: action.walletId,
|
|
1971
|
+
connectingNewAccount: false,
|
|
1972
|
+
step: "deposit"
|
|
1973
|
+
};
|
|
1974
|
+
// ── Transfer lifecycle ───────────────────────────────────────
|
|
1975
|
+
case "PAY_STARTED":
|
|
1976
|
+
return {
|
|
1977
|
+
...state,
|
|
1978
|
+
step: action.isSetupRedirect ? "open-wallet" : "processing",
|
|
1979
|
+
error: null,
|
|
1980
|
+
creatingTransfer: true,
|
|
1981
|
+
deeplinkUri: null,
|
|
1982
|
+
mobileFlow: false
|
|
1983
|
+
};
|
|
1984
|
+
case "PAY_ENDED":
|
|
1985
|
+
return { ...state, creatingTransfer: false };
|
|
1986
|
+
case "PAY_ERROR":
|
|
1987
|
+
return {
|
|
1988
|
+
...state,
|
|
1989
|
+
error: action.error,
|
|
1990
|
+
step: action.fallbackStep
|
|
1991
|
+
};
|
|
1992
|
+
case "TRANSFER_CREATED":
|
|
1993
|
+
return { ...state, transfer: action.transfer };
|
|
1994
|
+
case "TRANSFER_SIGNED":
|
|
1995
|
+
return { ...state, transfer: action.transfer };
|
|
1996
|
+
case "TRANSFER_COMPLETED":
|
|
1997
|
+
return {
|
|
1998
|
+
...state,
|
|
1999
|
+
transfer: action.transfer,
|
|
2000
|
+
step: "success",
|
|
2001
|
+
mobileFlow: false,
|
|
2002
|
+
deeplinkUri: null
|
|
2003
|
+
};
|
|
2004
|
+
case "TRANSFER_FAILED":
|
|
2005
|
+
return {
|
|
2006
|
+
...state,
|
|
2007
|
+
transfer: action.transfer,
|
|
2008
|
+
error: action.error,
|
|
2009
|
+
step: "success",
|
|
2010
|
+
mobileFlow: false,
|
|
2011
|
+
deeplinkUri: null
|
|
2012
|
+
};
|
|
2013
|
+
case "PROCESSING_TIMEOUT":
|
|
2014
|
+
return { ...state, error: action.error, step: "deposit" };
|
|
2015
|
+
case "CONFIRM_SIGN_SUCCESS":
|
|
2016
|
+
return {
|
|
2017
|
+
...state,
|
|
2018
|
+
transfer: action.transfer,
|
|
2019
|
+
step: "processing",
|
|
2020
|
+
mobileFlow: false,
|
|
2021
|
+
deeplinkUri: null
|
|
2022
|
+
};
|
|
2023
|
+
// ── Mobile flow ──────────────────────────────────────────────
|
|
2024
|
+
case "MOBILE_DEEPLINK_READY":
|
|
2025
|
+
return {
|
|
2026
|
+
...state,
|
|
2027
|
+
mobileFlow: true,
|
|
2028
|
+
deeplinkUri: action.deeplinkUri,
|
|
2029
|
+
step: "open-wallet"
|
|
2030
|
+
};
|
|
2031
|
+
case "MOBILE_SETUP_COMPLETE":
|
|
2032
|
+
return {
|
|
2033
|
+
...state,
|
|
2034
|
+
transfer: action.transfer,
|
|
2035
|
+
error: null,
|
|
2036
|
+
mobileFlow: false,
|
|
2037
|
+
deeplinkUri: null,
|
|
2038
|
+
step: "deposit"
|
|
2039
|
+
};
|
|
2040
|
+
case "MOBILE_SIGN_READY":
|
|
2041
|
+
return {
|
|
2042
|
+
...state,
|
|
2043
|
+
transfer: action.transfer,
|
|
2044
|
+
error: null,
|
|
2045
|
+
mobileFlow: false,
|
|
2046
|
+
deeplinkUri: null,
|
|
2047
|
+
step: "confirm-sign"
|
|
2048
|
+
};
|
|
2049
|
+
case "CLEAR_MOBILE_STATE":
|
|
2050
|
+
return { ...state, mobileFlow: false, deeplinkUri: null };
|
|
2051
|
+
case "ENTER_MOBILE_FLOW":
|
|
2052
|
+
return {
|
|
2053
|
+
...state,
|
|
2054
|
+
mobileFlow: true,
|
|
2055
|
+
deeplinkUri: action.deeplinkUri,
|
|
2056
|
+
selectedProviderId: action.providerId,
|
|
2057
|
+
error: action.error ?? null,
|
|
2058
|
+
step: "open-wallet"
|
|
2059
|
+
};
|
|
2060
|
+
case "MOBILE_RESUME_SUCCESS":
|
|
2061
|
+
return {
|
|
2062
|
+
...state,
|
|
2063
|
+
transfer: action.transfer,
|
|
2064
|
+
error: null,
|
|
2065
|
+
mobileFlow: false,
|
|
2066
|
+
deeplinkUri: null,
|
|
2067
|
+
step: "success"
|
|
2068
|
+
};
|
|
2069
|
+
case "MOBILE_RESUME_FAILED":
|
|
2070
|
+
return {
|
|
2071
|
+
...state,
|
|
2072
|
+
transfer: action.transfer,
|
|
2073
|
+
error: "Transfer failed.",
|
|
2074
|
+
mobileFlow: false,
|
|
2075
|
+
deeplinkUri: null,
|
|
2076
|
+
step: "success"
|
|
2077
|
+
};
|
|
2078
|
+
case "MOBILE_RESUME_PROCESSING":
|
|
2079
|
+
return {
|
|
2080
|
+
...state,
|
|
2081
|
+
transfer: action.transfer,
|
|
2082
|
+
error: null,
|
|
2083
|
+
mobileFlow: false,
|
|
2084
|
+
deeplinkUri: null,
|
|
2085
|
+
step: "processing"
|
|
2086
|
+
};
|
|
2087
|
+
// ── Increase limit ───────────────────────────────────────────
|
|
2088
|
+
case "SET_INCREASING_LIMIT":
|
|
2089
|
+
return { ...state, increasingLimit: action.value };
|
|
2090
|
+
case "INCREASE_LIMIT_DEEPLINK":
|
|
2091
|
+
return {
|
|
2092
|
+
...state,
|
|
2093
|
+
transfer: action.transfer,
|
|
2094
|
+
mobileFlow: true,
|
|
2095
|
+
deeplinkUri: action.deeplinkUri
|
|
2096
|
+
};
|
|
2097
|
+
// ── Navigation & error ───────────────────────────────────────
|
|
2098
|
+
case "NAVIGATE":
|
|
2099
|
+
return { ...state, step: action.step };
|
|
2100
|
+
case "SET_ERROR":
|
|
2101
|
+
return { ...state, error: action.error };
|
|
2102
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
2103
|
+
case "NEW_PAYMENT":
|
|
2104
|
+
return {
|
|
2105
|
+
...state,
|
|
2106
|
+
step: "deposit",
|
|
2107
|
+
transfer: null,
|
|
2108
|
+
error: null,
|
|
2109
|
+
amount: action.depositAmount != null ? action.depositAmount.toString() : "",
|
|
2110
|
+
mobileFlow: false,
|
|
2111
|
+
deeplinkUri: null,
|
|
2112
|
+
connectingNewAccount: false,
|
|
2113
|
+
selectedWalletId: null,
|
|
2114
|
+
selectedAccountId: action.firstAccountId
|
|
2115
|
+
};
|
|
2116
|
+
case "LOGOUT":
|
|
2117
|
+
return {
|
|
2118
|
+
...createInitialState({
|
|
2119
|
+
depositAmount: action.depositAmount,
|
|
2120
|
+
passkeyPopupNeeded: state.passkeyPopupNeeded,
|
|
2121
|
+
activeCredentialId: null
|
|
2122
|
+
})
|
|
2123
|
+
};
|
|
2124
|
+
case "SYNC_AMOUNT":
|
|
2125
|
+
return { ...state, amount: action.amount };
|
|
2126
|
+
default:
|
|
2127
|
+
return state;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
function Spinner({ size = 40, label }) {
|
|
2131
|
+
const { tokens } = useSwypeConfig();
|
|
2132
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2133
|
+
"div",
|
|
2134
|
+
{
|
|
2135
|
+
style: {
|
|
2136
|
+
display: "flex",
|
|
2137
|
+
flexDirection: "column",
|
|
2138
|
+
alignItems: "center",
|
|
2139
|
+
gap: "12px"
|
|
2140
|
+
},
|
|
2141
|
+
children: [
|
|
2142
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2143
|
+
"div",
|
|
2144
|
+
{
|
|
2145
|
+
style: {
|
|
2146
|
+
width: size,
|
|
2147
|
+
height: size,
|
|
2148
|
+
border: `4px solid ${tokens.bgInput}`,
|
|
2149
|
+
borderTopColor: tokens.accent,
|
|
2150
|
+
borderRightColor: tokens.accent + "66",
|
|
2151
|
+
borderRadius: "50%",
|
|
2152
|
+
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.1)",
|
|
2153
|
+
animation: "swype-spin 0.9s linear infinite"
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
),
|
|
2157
|
+
label && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: tokens.textSecondary, fontSize: "0.875rem", margin: 0 }, children: label }),
|
|
2158
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `
|
|
2159
|
+
@keyframes swype-spin {
|
|
2160
|
+
to { transform: rotate(360deg); }
|
|
2161
|
+
}
|
|
2162
|
+
` })
|
|
2163
|
+
]
|
|
2164
|
+
}
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
1849
2167
|
var FOOTER_CSS = `
|
|
1850
2168
|
.swype-screen-footer {
|
|
1851
2169
|
padding-bottom: max(24px, env(safe-area-inset-bottom, 24px));
|
|
@@ -4562,11 +4880,248 @@ var errorStyle2 = (color) => ({
|
|
|
4562
4880
|
color: "#ef4444",
|
|
4563
4881
|
margin: "8px 0 0"
|
|
4564
4882
|
});
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4883
|
+
function CenteredSpinner({ label }) {
|
|
4884
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label }) }) });
|
|
4885
|
+
}
|
|
4886
|
+
function StepRenderer({
|
|
4887
|
+
state,
|
|
4888
|
+
ready,
|
|
4889
|
+
authenticated,
|
|
4890
|
+
activeOtpStatus,
|
|
4891
|
+
pollingTransfer,
|
|
4892
|
+
pollingError,
|
|
4893
|
+
authExecutorError,
|
|
4894
|
+
transferSigningSigning,
|
|
4895
|
+
transferSigningError,
|
|
4896
|
+
pendingConnections,
|
|
4897
|
+
sourceName,
|
|
4898
|
+
sourceAddress,
|
|
4899
|
+
sourceVerified,
|
|
4900
|
+
maxSourceBalance,
|
|
4901
|
+
tokenCount,
|
|
4902
|
+
selectedAccount,
|
|
4903
|
+
selectSourceChoices,
|
|
4904
|
+
selectSourceRecommended,
|
|
4905
|
+
authInput,
|
|
4906
|
+
otpCode,
|
|
4907
|
+
selectSourceChainName,
|
|
4908
|
+
selectSourceTokenSymbol,
|
|
4909
|
+
merchantName,
|
|
4910
|
+
onBack,
|
|
4911
|
+
onDismiss,
|
|
4912
|
+
autoCloseSeconds,
|
|
4913
|
+
depositAmount,
|
|
4914
|
+
handlers
|
|
4915
|
+
}) {
|
|
4916
|
+
const { step } = state;
|
|
4917
|
+
if (!ready) {
|
|
4918
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Initializing..." });
|
|
4919
|
+
}
|
|
4920
|
+
if (step === "login" && !authenticated) {
|
|
4921
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4922
|
+
LoginScreen,
|
|
4923
|
+
{
|
|
4924
|
+
authInput,
|
|
4925
|
+
onAuthInputChange: handlers.onSetAuthInput,
|
|
4926
|
+
onSubmit: handlers.onSendLoginCode,
|
|
4927
|
+
sending: activeOtpStatus === "sending-code",
|
|
4928
|
+
error: state.error,
|
|
4929
|
+
onBack,
|
|
4930
|
+
merchantInitials: merchantName ? merchantName.slice(0, 2).toUpperCase() : void 0
|
|
4931
|
+
}
|
|
4932
|
+
);
|
|
4933
|
+
}
|
|
4934
|
+
if (step === "otp-verify" && !authenticated) {
|
|
4935
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4936
|
+
OtpVerifyScreen,
|
|
4937
|
+
{
|
|
4938
|
+
maskedIdentifier: state.verificationTarget ? maskAuthIdentifier(state.verificationTarget) : "",
|
|
4939
|
+
otpCode,
|
|
4940
|
+
onOtpChange: (code) => {
|
|
4941
|
+
handlers.onSetOtpCode(code);
|
|
4942
|
+
},
|
|
4943
|
+
onVerify: handlers.onVerifyLoginCode,
|
|
4944
|
+
onResend: handlers.onResendLoginCode,
|
|
4945
|
+
onBack: handlers.onBackFromOtp,
|
|
4946
|
+
verifying: activeOtpStatus === "submitting-code",
|
|
4947
|
+
error: state.error
|
|
4948
|
+
}
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
if ((step === "login" || step === "otp-verify") && authenticated) {
|
|
4952
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Verifying your passkey..." });
|
|
4953
|
+
}
|
|
4954
|
+
if (step === "verify-passkey") {
|
|
4955
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4956
|
+
VerifyPasskeyScreen,
|
|
4957
|
+
{
|
|
4958
|
+
onVerify: handlers.onVerifyPasskeyViaPopup,
|
|
4959
|
+
onBack: handlers.onLogout,
|
|
4960
|
+
verifying: state.verifyingPasskeyPopup,
|
|
4961
|
+
error: state.error
|
|
4962
|
+
}
|
|
4963
|
+
);
|
|
4964
|
+
}
|
|
4965
|
+
if (step === "create-passkey") {
|
|
4966
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4967
|
+
CreatePasskeyScreen,
|
|
4968
|
+
{
|
|
4969
|
+
onCreatePasskey: handlers.onRegisterPasskey,
|
|
4970
|
+
onBack: handlers.onLogout,
|
|
4971
|
+
creating: state.registeringPasskey,
|
|
4972
|
+
error: state.error,
|
|
4973
|
+
popupFallback: state.passkeyPopupNeeded,
|
|
4974
|
+
onCreatePasskeyViaPopup: handlers.onCreatePasskeyViaPopup
|
|
4975
|
+
}
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
if (step === "wallet-picker") {
|
|
4979
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4980
|
+
WalletPickerScreen,
|
|
4981
|
+
{
|
|
4982
|
+
providers: state.providers,
|
|
4983
|
+
pendingConnections,
|
|
4984
|
+
loading: state.creatingTransfer,
|
|
4985
|
+
onSelectProvider: handlers.onSelectProvider,
|
|
4986
|
+
onContinueConnection: handlers.onContinueConnection,
|
|
4987
|
+
onBack: () => handlers.onNavigate(state.activeCredentialId ? "deposit" : "create-passkey")
|
|
4988
|
+
}
|
|
4989
|
+
);
|
|
4990
|
+
}
|
|
4991
|
+
if (step === "open-wallet") {
|
|
4992
|
+
const providerName = state.providers.find((p) => p.id === state.selectedProviderId)?.name ?? null;
|
|
4993
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4994
|
+
OpenWalletScreen,
|
|
4995
|
+
{
|
|
4996
|
+
walletName: providerName,
|
|
4997
|
+
deeplinkUri: state.deeplinkUri ?? "",
|
|
4998
|
+
loading: state.creatingTransfer || !state.deeplinkUri,
|
|
4999
|
+
error: state.error || pollingError,
|
|
5000
|
+
onRetryStatus: handlers.onRetryMobileStatus,
|
|
5001
|
+
onLogout: handlers.onLogout
|
|
5002
|
+
}
|
|
5003
|
+
);
|
|
5004
|
+
}
|
|
5005
|
+
if (step === "confirm-sign") {
|
|
5006
|
+
const providerName = state.providers.find((p) => p.id === state.selectedProviderId)?.name ?? null;
|
|
5007
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5008
|
+
ConfirmSignScreen,
|
|
5009
|
+
{
|
|
5010
|
+
walletName: providerName,
|
|
5011
|
+
signing: transferSigningSigning,
|
|
5012
|
+
error: state.error || transferSigningError,
|
|
5013
|
+
onSign: handlers.onConfirmSign,
|
|
5014
|
+
onLogout: handlers.onLogout
|
|
5015
|
+
}
|
|
5016
|
+
);
|
|
5017
|
+
}
|
|
5018
|
+
if (step === "deposit") {
|
|
5019
|
+
if (state.loadingData) {
|
|
5020
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Loading..." });
|
|
5021
|
+
}
|
|
5022
|
+
const parsedAmt = depositAmount != null ? depositAmount : 5;
|
|
5023
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5024
|
+
DepositScreen,
|
|
5025
|
+
{
|
|
5026
|
+
merchantName,
|
|
5027
|
+
sourceName,
|
|
5028
|
+
sourceAddress,
|
|
5029
|
+
sourceVerified,
|
|
5030
|
+
availableBalance: maxSourceBalance,
|
|
5031
|
+
remainingLimit: selectedAccount?.remainingAllowance ?? state.oneTapLimit,
|
|
5032
|
+
tokenCount,
|
|
5033
|
+
initialAmount: parsedAmt,
|
|
5034
|
+
processing: state.creatingTransfer,
|
|
5035
|
+
error: state.error,
|
|
5036
|
+
onDeposit: handlers.onPay,
|
|
5037
|
+
onChangeSource: () => handlers.onNavigate("wallet-picker"),
|
|
5038
|
+
onSwitchWallet: () => handlers.onNavigate("wallet-picker"),
|
|
5039
|
+
onBack: onBack ?? (() => handlers.onLogout()),
|
|
5040
|
+
onLogout: handlers.onLogout,
|
|
5041
|
+
onIncreaseLimit: handlers.onIncreaseLimit,
|
|
5042
|
+
increasingLimit: state.increasingLimit
|
|
5043
|
+
}
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
if (step === "processing") {
|
|
5047
|
+
const polledStatus = pollingTransfer?.status;
|
|
5048
|
+
const transferPhase = state.creatingTransfer ? "creating" : polledStatus === "SENDING" || polledStatus === "SENT" ? "sent" : "verifying";
|
|
5049
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5050
|
+
TransferStatusScreen,
|
|
5051
|
+
{
|
|
5052
|
+
phase: transferPhase,
|
|
5053
|
+
error: state.error || authExecutorError || transferSigningError || pollingError,
|
|
5054
|
+
onLogout: handlers.onLogout
|
|
5055
|
+
}
|
|
5056
|
+
);
|
|
5057
|
+
}
|
|
5058
|
+
if (step === "select-source") {
|
|
5059
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5060
|
+
SelectSourceScreen,
|
|
5061
|
+
{
|
|
5062
|
+
choices: selectSourceChoices,
|
|
5063
|
+
selectedChainName: selectSourceChainName,
|
|
5064
|
+
selectedTokenSymbol: selectSourceTokenSymbol,
|
|
5065
|
+
recommended: selectSourceRecommended,
|
|
5066
|
+
onChainChange: handlers.onSelectSourceChainChange,
|
|
5067
|
+
onTokenChange: handlers.onSetSelectSourceTokenSymbol,
|
|
5068
|
+
onConfirm: handlers.onConfirmSelectSource,
|
|
5069
|
+
onLogout: handlers.onLogout
|
|
5070
|
+
}
|
|
5071
|
+
);
|
|
5072
|
+
}
|
|
5073
|
+
if (step === "success") {
|
|
5074
|
+
const succeeded = state.transfer?.status === "COMPLETED";
|
|
5075
|
+
const displayAmount = state.transfer?.amount?.amount ?? 0;
|
|
5076
|
+
const displayCurrency = state.transfer?.amount?.currency ?? "USD";
|
|
5077
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5078
|
+
SuccessScreen,
|
|
5079
|
+
{
|
|
5080
|
+
amount: displayAmount,
|
|
5081
|
+
currency: displayCurrency,
|
|
5082
|
+
succeeded,
|
|
5083
|
+
error: state.error,
|
|
5084
|
+
merchantName,
|
|
5085
|
+
sourceName,
|
|
5086
|
+
remainingLimit: succeeded ? (() => {
|
|
5087
|
+
const limit = selectedAccount?.remainingAllowance ?? state.oneTapLimit;
|
|
5088
|
+
return limit > displayAmount ? limit - displayAmount : 0;
|
|
5089
|
+
})() : void 0,
|
|
5090
|
+
onDone: onDismiss ?? handlers.onNewPayment,
|
|
5091
|
+
onLogout: handlers.onLogout,
|
|
5092
|
+
autoCloseSeconds
|
|
5093
|
+
}
|
|
5094
|
+
);
|
|
5095
|
+
}
|
|
5096
|
+
if (step === "low-balance") {
|
|
5097
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5098
|
+
DepositScreen,
|
|
5099
|
+
{
|
|
5100
|
+
merchantName,
|
|
5101
|
+
sourceName,
|
|
5102
|
+
sourceAddress,
|
|
5103
|
+
sourceVerified,
|
|
5104
|
+
availableBalance: 0,
|
|
5105
|
+
remainingLimit: selectedAccount?.remainingAllowance ?? state.oneTapLimit,
|
|
5106
|
+
tokenCount,
|
|
5107
|
+
initialAmount: depositAmount ?? 5,
|
|
5108
|
+
processing: false,
|
|
5109
|
+
error: state.error,
|
|
5110
|
+
onDeposit: handlers.onPay,
|
|
5111
|
+
onChangeSource: () => handlers.onNavigate("wallet-picker"),
|
|
5112
|
+
onSwitchWallet: () => handlers.onNavigate("wallet-picker"),
|
|
5113
|
+
onBack: onBack ?? (() => handlers.onLogout()),
|
|
5114
|
+
onLogout: handlers.onLogout
|
|
5115
|
+
}
|
|
5116
|
+
);
|
|
5117
|
+
}
|
|
5118
|
+
return null;
|
|
5119
|
+
}
|
|
5120
|
+
var PaymentErrorBoundary = class extends react.Component {
|
|
5121
|
+
constructor(props) {
|
|
5122
|
+
super(props);
|
|
5123
|
+
this.state = { hasError: false };
|
|
5124
|
+
}
|
|
4570
5125
|
static getDerivedStateFromError() {
|
|
4571
5126
|
return { hasError: true };
|
|
4572
5127
|
}
|
|
@@ -4630,98 +5185,6 @@ var buttonStyle3 = {
|
|
|
4630
5185
|
fontFamily: "inherit",
|
|
4631
5186
|
cursor: "pointer"
|
|
4632
5187
|
};
|
|
4633
|
-
var ACTIVE_CREDENTIAL_STORAGE_KEY = "swype_active_credential_id";
|
|
4634
|
-
var MOBILE_FLOW_STORAGE_KEY = "swype_mobile_flow";
|
|
4635
|
-
var MIN_SEND_AMOUNT_USD = 0.25;
|
|
4636
|
-
function persistMobileFlowState(data) {
|
|
4637
|
-
try {
|
|
4638
|
-
sessionStorage.setItem(MOBILE_FLOW_STORAGE_KEY, JSON.stringify(data));
|
|
4639
|
-
} catch {
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
function loadMobileFlowState() {
|
|
4643
|
-
try {
|
|
4644
|
-
const raw = sessionStorage.getItem(MOBILE_FLOW_STORAGE_KEY);
|
|
4645
|
-
if (!raw) return null;
|
|
4646
|
-
return JSON.parse(raw);
|
|
4647
|
-
} catch {
|
|
4648
|
-
return null;
|
|
4649
|
-
}
|
|
4650
|
-
}
|
|
4651
|
-
function clearMobileFlowState() {
|
|
4652
|
-
try {
|
|
4653
|
-
sessionStorage.removeItem(MOBILE_FLOW_STORAGE_KEY);
|
|
4654
|
-
} catch {
|
|
4655
|
-
}
|
|
4656
|
-
}
|
|
4657
|
-
function computeSmartDefaults(accts, transferAmount) {
|
|
4658
|
-
if (accts.length === 0) return null;
|
|
4659
|
-
for (const acct of accts) {
|
|
4660
|
-
for (const wallet of acct.wallets) {
|
|
4661
|
-
if (wallet.status === "ACTIVE") {
|
|
4662
|
-
const bestSource = wallet.sources.find(
|
|
4663
|
-
(s) => s.balance.available.amount >= transferAmount
|
|
4664
|
-
);
|
|
4665
|
-
if (bestSource) {
|
|
4666
|
-
return { accountId: acct.id, walletId: wallet.id };
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
}
|
|
4671
|
-
let bestAccount = null;
|
|
4672
|
-
let bestWallet = null;
|
|
4673
|
-
let bestBalance = -1;
|
|
4674
|
-
let bestIsActive = false;
|
|
4675
|
-
for (const acct of accts) {
|
|
4676
|
-
for (const wallet of acct.wallets) {
|
|
4677
|
-
const walletBal = wallet.balance.available.amount;
|
|
4678
|
-
const isActive = wallet.status === "ACTIVE";
|
|
4679
|
-
if (walletBal > bestBalance || walletBal === bestBalance && isActive && !bestIsActive) {
|
|
4680
|
-
bestBalance = walletBal;
|
|
4681
|
-
bestAccount = acct;
|
|
4682
|
-
bestWallet = wallet;
|
|
4683
|
-
bestIsActive = isActive;
|
|
4684
|
-
}
|
|
4685
|
-
}
|
|
4686
|
-
}
|
|
4687
|
-
if (bestAccount) {
|
|
4688
|
-
return {
|
|
4689
|
-
accountId: bestAccount.id,
|
|
4690
|
-
walletId: bestWallet?.id ?? null
|
|
4691
|
-
};
|
|
4692
|
-
}
|
|
4693
|
-
return { accountId: accts[0].id, walletId: null };
|
|
4694
|
-
}
|
|
4695
|
-
function parseRawBalance(rawBalance, decimals) {
|
|
4696
|
-
const parsed = Number(rawBalance);
|
|
4697
|
-
if (!Number.isFinite(parsed)) return 0;
|
|
4698
|
-
return parsed / 10 ** decimals;
|
|
4699
|
-
}
|
|
4700
|
-
function buildSelectSourceChoices(options) {
|
|
4701
|
-
const chainChoices = [];
|
|
4702
|
-
const chainIndexByName = /* @__PURE__ */ new Map();
|
|
4703
|
-
for (const option of options) {
|
|
4704
|
-
const { chainName, tokenSymbol } = option;
|
|
4705
|
-
const balance = parseRawBalance(option.rawBalance, option.decimals);
|
|
4706
|
-
let chainChoice;
|
|
4707
|
-
const existingIdx = chainIndexByName.get(chainName);
|
|
4708
|
-
if (existingIdx === void 0) {
|
|
4709
|
-
chainChoice = { chainName, balance: 0, tokens: [] };
|
|
4710
|
-
chainIndexByName.set(chainName, chainChoices.length);
|
|
4711
|
-
chainChoices.push(chainChoice);
|
|
4712
|
-
} else {
|
|
4713
|
-
chainChoice = chainChoices[existingIdx];
|
|
4714
|
-
}
|
|
4715
|
-
chainChoice.balance += balance;
|
|
4716
|
-
const existing = chainChoice.tokens.find((t) => t.tokenSymbol === tokenSymbol);
|
|
4717
|
-
if (existing) {
|
|
4718
|
-
existing.balance += balance;
|
|
4719
|
-
} else {
|
|
4720
|
-
chainChoice.tokens.push({ tokenSymbol, balance });
|
|
4721
|
-
}
|
|
4722
|
-
}
|
|
4723
|
-
return chainChoices;
|
|
4724
|
-
}
|
|
4725
5188
|
function SwypePayment(props) {
|
|
4726
5189
|
const resetKey = react.useRef(0);
|
|
4727
5190
|
const handleBoundaryReset = react.useCallback(() => {
|
|
@@ -4733,7 +5196,7 @@ function SwypePaymentInner({
|
|
|
4733
5196
|
destination,
|
|
4734
5197
|
onComplete,
|
|
4735
5198
|
onError,
|
|
4736
|
-
useWalletConnector,
|
|
5199
|
+
useWalletConnector: useWalletConnectorProp,
|
|
4737
5200
|
idempotencyKey,
|
|
4738
5201
|
merchantAuthorization,
|
|
4739
5202
|
merchantName,
|
|
@@ -4741,7 +5204,7 @@ function SwypePaymentInner({
|
|
|
4741
5204
|
onDismiss,
|
|
4742
5205
|
autoCloseSeconds
|
|
4743
5206
|
}) {
|
|
4744
|
-
const { apiBaseUrl,
|
|
5207
|
+
const { apiBaseUrl, depositAmount } = useSwypeConfig();
|
|
4745
5208
|
const { ready, authenticated, user, logout, getAccessToken } = reactAuth.usePrivy();
|
|
4746
5209
|
const {
|
|
4747
5210
|
sendCode: sendEmailCode,
|
|
@@ -4753,161 +5216,74 @@ function SwypePaymentInner({
|
|
|
4753
5216
|
loginWithCode: loginWithSmsCode,
|
|
4754
5217
|
state: smsLoginState
|
|
4755
5218
|
} = reactAuth.useLoginWithSms();
|
|
4756
|
-
|
|
4757
|
-
const [
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
const [selectedProviderId, setSelectedProviderId] = react.useState(null);
|
|
4766
|
-
const [connectingNewAccount, setConnectingNewAccount] = react.useState(false);
|
|
4767
|
-
const [amount, setAmount] = react.useState(
|
|
4768
|
-
depositAmount != null ? depositAmount.toString() : ""
|
|
4769
|
-
);
|
|
4770
|
-
const [transfer, setTransfer] = react.useState(null);
|
|
4771
|
-
const [creatingTransfer, setCreatingTransfer] = react.useState(false);
|
|
4772
|
-
const [registeringPasskey, setRegisteringPasskey] = react.useState(false);
|
|
4773
|
-
const [verifyingPasskeyPopup, setVerifyingPasskeyPopup] = react.useState(false);
|
|
4774
|
-
const [passkeyPopupNeeded, setPasskeyPopupNeeded] = react.useState(
|
|
4775
|
-
() => isSafari() && isInCrossOriginIframe()
|
|
5219
|
+
reactAuth.useLoginWithOAuth();
|
|
5220
|
+
const [state, dispatch] = react.useReducer(
|
|
5221
|
+
paymentReducer,
|
|
5222
|
+
{
|
|
5223
|
+
depositAmount,
|
|
5224
|
+
passkeyPopupNeeded: isSafari() && isInCrossOriginIframe(),
|
|
5225
|
+
activeCredentialId: typeof window === "undefined" ? null : window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY)
|
|
5226
|
+
},
|
|
5227
|
+
createInitialState
|
|
4776
5228
|
);
|
|
4777
|
-
const [activeCredentialId, setActiveCredentialId] = react.useState(() => {
|
|
4778
|
-
if (typeof window === "undefined") return null;
|
|
4779
|
-
return window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
|
|
4780
|
-
});
|
|
4781
|
-
const [knownCredentialIds, setKnownCredentialIds] = react.useState([]);
|
|
4782
|
-
const [authInput, setAuthInput] = react.useState("");
|
|
4783
|
-
const [verificationTarget, setVerificationTarget] = react.useState(null);
|
|
4784
|
-
const [otpCode, setOtpCode] = react.useState("");
|
|
4785
|
-
const [oneTapLimit, setOneTapLimit] = react.useState(100);
|
|
4786
|
-
const [mobileFlow, setMobileFlow] = react.useState(false);
|
|
4787
|
-
const [deeplinkUri, setDeeplinkUri] = react.useState(null);
|
|
4788
5229
|
const loadingDataRef = react.useRef(false);
|
|
4789
5230
|
const pollingTransferIdRef = react.useRef(null);
|
|
4790
|
-
const mobileSigningTransferIdRef = react.useRef(null);
|
|
4791
5231
|
const mobileSetupFlowRef = react.useRef(false);
|
|
4792
5232
|
const handlingMobileReturnRef = react.useRef(false);
|
|
4793
5233
|
const processingStartedAtRef = react.useRef(null);
|
|
4794
|
-
const [selectSourceChainName, setSelectSourceChainName] = react.useState("");
|
|
4795
|
-
const [selectSourceTokenSymbol, setSelectSourceTokenSymbol] = react.useState("");
|
|
4796
5234
|
const initializedSelectSourceActionRef = react.useRef(null);
|
|
4797
5235
|
const preSelectSourceStepRef = react.useRef(null);
|
|
4798
|
-
const
|
|
4799
|
-
const
|
|
4800
|
-
const
|
|
4801
|
-
const
|
|
4802
|
-
const
|
|
4803
|
-
const
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
pollingTransferIdRef.current = persisted.transferId;
|
|
4829
|
-
mobileSetupFlowRef.current = persisted.isSetup;
|
|
4830
|
-
setError(errorMessage ?? null);
|
|
4831
|
-
setStep("open-wallet");
|
|
4832
|
-
polling.startPolling(persisted.transferId);
|
|
4833
|
-
}, [polling]);
|
|
4834
|
-
const handleAuthorizedMobileReturn = react.useCallback(async (authorizedTransfer, isSetup) => {
|
|
4835
|
-
if (handlingMobileReturnRef.current) return;
|
|
4836
|
-
handlingMobileReturnRef.current = true;
|
|
4837
|
-
polling.stopPolling();
|
|
4838
|
-
if (isSetup) {
|
|
4839
|
-
mobileSetupFlowRef.current = false;
|
|
4840
|
-
clearMobileFlowState();
|
|
4841
|
-
try {
|
|
4842
|
-
await reloadAccounts();
|
|
4843
|
-
resetDataLoadingState();
|
|
4844
|
-
setTransfer(authorizedTransfer);
|
|
4845
|
-
setError(null);
|
|
4846
|
-
setDeeplinkUri(null);
|
|
4847
|
-
setMobileFlow(false);
|
|
4848
|
-
setStep("deposit");
|
|
4849
|
-
} catch (err) {
|
|
4850
|
-
handlingMobileReturnRef.current = false;
|
|
4851
|
-
setError(
|
|
4852
|
-
err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
|
|
4853
|
-
);
|
|
4854
|
-
setStep("open-wallet");
|
|
5236
|
+
const [authInput, setAuthInput] = react.useState("");
|
|
5237
|
+
const [otpCode, setOtpCode] = react.useState("");
|
|
5238
|
+
const [selectSourceChainName, setSelectSourceChainName] = react.useState("");
|
|
5239
|
+
const [selectSourceTokenSymbol, setSelectSourceTokenSymbol] = react.useState("");
|
|
5240
|
+
const authExecutor = useAuthorizationExecutor();
|
|
5241
|
+
const polling = useTransferPolling();
|
|
5242
|
+
const transferSigning = useTransferSigning();
|
|
5243
|
+
const { sourceType, sourceId } = deriveSourceTypeAndId(state);
|
|
5244
|
+
const selectedAccount = state.accounts.find((a) => a.id === state.selectedAccountId);
|
|
5245
|
+
const selectedWallet = selectedAccount?.wallets.find(
|
|
5246
|
+
(w) => w.id === state.selectedWalletId
|
|
5247
|
+
);
|
|
5248
|
+
const sourceName = selectedAccount?.name ?? selectedWallet?.chain.name ?? "Wallet";
|
|
5249
|
+
const sourceAddress = selectedWallet ? `${selectedWallet.name.slice(0, 6)}...${selectedWallet.name.slice(-4)}` : void 0;
|
|
5250
|
+
const sourceVerified = selectedWallet?.status === "ACTIVE";
|
|
5251
|
+
const pendingConnections = react.useMemo(
|
|
5252
|
+
() => state.accounts.filter(
|
|
5253
|
+
(a) => a.wallets.length > 0 && !a.wallets.some((w) => w.status === "ACTIVE")
|
|
5254
|
+
),
|
|
5255
|
+
[state.accounts]
|
|
5256
|
+
);
|
|
5257
|
+
const maxSourceBalance = react.useMemo(() => {
|
|
5258
|
+
let max = 0;
|
|
5259
|
+
for (const acct of state.accounts) {
|
|
5260
|
+
for (const wallet of acct.wallets) {
|
|
5261
|
+
for (const source of wallet.sources) {
|
|
5262
|
+
if (source.balance.available.amount > max) {
|
|
5263
|
+
max = source.balance.available.amount;
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
4855
5266
|
}
|
|
4856
|
-
return;
|
|
4857
|
-
}
|
|
4858
|
-
setTransfer(authorizedTransfer);
|
|
4859
|
-
mobileSetupFlowRef.current = false;
|
|
4860
|
-
clearMobileFlowState();
|
|
4861
|
-
setError(null);
|
|
4862
|
-
setDeeplinkUri(null);
|
|
4863
|
-
setMobileFlow(false);
|
|
4864
|
-
setStep("confirm-sign");
|
|
4865
|
-
}, [polling.stopPolling, reloadAccounts, resetDataLoadingState]);
|
|
4866
|
-
const handleRetryMobileStatus = react.useCallback(() => {
|
|
4867
|
-
setError(null);
|
|
4868
|
-
handlingMobileReturnRef.current = false;
|
|
4869
|
-
const currentTransfer = polling.transfer ?? transfer;
|
|
4870
|
-
if (currentTransfer?.status === "AUTHORIZED") {
|
|
4871
|
-
void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
|
|
4872
|
-
return;
|
|
4873
|
-
}
|
|
4874
|
-
const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
|
|
4875
|
-
if (transferIdToResume) {
|
|
4876
|
-
polling.startPolling(transferIdToResume);
|
|
4877
|
-
}
|
|
4878
|
-
}, [handleAuthorizedMobileReturn, polling, transfer]);
|
|
4879
|
-
react.useEffect(() => {
|
|
4880
|
-
if (depositAmount != null) {
|
|
4881
|
-
setAmount(depositAmount.toString());
|
|
4882
5267
|
}
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
try {
|
|
4892
|
-
await initOAuth({ provider });
|
|
4893
|
-
} catch (err) {
|
|
4894
|
-
captureException(err);
|
|
4895
|
-
setError(err instanceof Error ? err.message : "Social login failed");
|
|
5268
|
+
return max;
|
|
5269
|
+
}, [state.accounts]);
|
|
5270
|
+
const tokenCount = react.useMemo(() => {
|
|
5271
|
+
let count = 0;
|
|
5272
|
+
for (const acct of state.accounts) {
|
|
5273
|
+
for (const wallet of acct.wallets) {
|
|
5274
|
+
count += wallet.sources.length;
|
|
5275
|
+
}
|
|
4896
5276
|
}
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
const
|
|
4900
|
-
|
|
4901
|
-
if (authenticated) return;
|
|
4902
|
-
if (activeOtpErrorMessage) setError(activeOtpErrorMessage);
|
|
4903
|
-
}, [activeOtpErrorMessage, authenticated]);
|
|
5277
|
+
return count;
|
|
5278
|
+
}, [state.accounts]);
|
|
5279
|
+
const activeOtpStatus = state.verificationTarget?.kind === "email" ? emailLoginState.status : state.verificationTarget?.kind === "phone" ? smsLoginState.status : "initial";
|
|
5280
|
+
const activeOtpErrorMessage = state.verificationTarget?.kind === "email" && emailLoginState.status === "error" ? emailLoginState.error?.message ?? "Failed to continue with email." : state.verificationTarget?.kind === "phone" && smsLoginState.status === "error" ? smsLoginState.error?.message ?? "Failed to continue with phone number." : null;
|
|
4904
5281
|
const handleSendLoginCode = react.useCallback(async () => {
|
|
4905
5282
|
const normalizedIdentifier = normalizeAuthIdentifier(authInput);
|
|
4906
5283
|
if (!normalizedIdentifier) {
|
|
4907
|
-
|
|
5284
|
+
dispatch({ type: "SET_ERROR", error: "Enter a valid email address or phone number." });
|
|
4908
5285
|
return;
|
|
4909
5286
|
}
|
|
4910
|
-
setError(null);
|
|
4911
5287
|
setOtpCode("");
|
|
4912
5288
|
try {
|
|
4913
5289
|
if (normalizedIdentifier.kind === "email") {
|
|
@@ -4915,468 +5291,214 @@ function SwypePaymentInner({
|
|
|
4915
5291
|
} else {
|
|
4916
5292
|
await sendSmsCode({ phoneNumber: normalizedIdentifier.value });
|
|
4917
5293
|
}
|
|
4918
|
-
|
|
4919
|
-
setStep("otp-verify");
|
|
5294
|
+
dispatch({ type: "CODE_SENT", target: normalizedIdentifier });
|
|
4920
5295
|
} catch (err) {
|
|
4921
5296
|
captureException(err);
|
|
4922
|
-
|
|
5297
|
+
dispatch({
|
|
5298
|
+
type: "SET_ERROR",
|
|
5299
|
+
error: err instanceof Error ? err.message : "Failed to send verification code"
|
|
5300
|
+
});
|
|
4923
5301
|
}
|
|
4924
5302
|
}, [authInput, sendEmailCode, sendSmsCode]);
|
|
4925
5303
|
const handleVerifyLoginCode = react.useCallback(async () => {
|
|
4926
|
-
if (!verificationTarget) return;
|
|
5304
|
+
if (!state.verificationTarget) return;
|
|
4927
5305
|
const trimmedCode = otpCode.trim();
|
|
4928
5306
|
if (!/^\d{6}$/.test(trimmedCode)) {
|
|
4929
|
-
|
|
5307
|
+
dispatch({ type: "SET_ERROR", error: "Enter the 6-digit verification code." });
|
|
4930
5308
|
return;
|
|
4931
5309
|
}
|
|
4932
|
-
|
|
5310
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
4933
5311
|
try {
|
|
4934
|
-
if (verificationTarget.kind === "email") {
|
|
5312
|
+
if (state.verificationTarget.kind === "email") {
|
|
4935
5313
|
await loginWithEmailCode({ code: trimmedCode });
|
|
4936
5314
|
} else {
|
|
4937
5315
|
await loginWithSmsCode({ code: trimmedCode });
|
|
4938
5316
|
}
|
|
4939
5317
|
} catch (err) {
|
|
4940
5318
|
captureException(err);
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
if (step === "otp-verify" && /^\d{6}$/.test(otpCode.trim()) && activeOtpStatus !== "submitting-code") {
|
|
4946
|
-
handleVerifyLoginCode();
|
|
5319
|
+
dispatch({
|
|
5320
|
+
type: "SET_ERROR",
|
|
5321
|
+
error: err instanceof Error ? err.message : "Failed to verify code"
|
|
5322
|
+
});
|
|
4947
5323
|
}
|
|
4948
|
-
}, [
|
|
5324
|
+
}, [state.verificationTarget, otpCode, loginWithEmailCode, loginWithSmsCode]);
|
|
4949
5325
|
const handleResendLoginCode = react.useCallback(async () => {
|
|
4950
|
-
if (!verificationTarget) return;
|
|
4951
|
-
|
|
5326
|
+
if (!state.verificationTarget) return;
|
|
5327
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
4952
5328
|
try {
|
|
4953
|
-
if (verificationTarget.kind === "email") {
|
|
4954
|
-
await sendEmailCode({ email: verificationTarget.value });
|
|
5329
|
+
if (state.verificationTarget.kind === "email") {
|
|
5330
|
+
await sendEmailCode({ email: state.verificationTarget.value });
|
|
4955
5331
|
} else {
|
|
4956
|
-
await sendSmsCode({ phoneNumber: verificationTarget.value });
|
|
5332
|
+
await sendSmsCode({ phoneNumber: state.verificationTarget.value });
|
|
4957
5333
|
}
|
|
4958
5334
|
} catch (err) {
|
|
4959
5335
|
captureException(err);
|
|
4960
|
-
|
|
5336
|
+
dispatch({
|
|
5337
|
+
type: "SET_ERROR",
|
|
5338
|
+
error: err instanceof Error ? err.message : "Failed to resend code"
|
|
5339
|
+
});
|
|
4961
5340
|
}
|
|
4962
|
-
}, [verificationTarget, sendEmailCode, sendSmsCode]);
|
|
4963
|
-
react.
|
|
4964
|
-
|
|
4965
|
-
if (
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
const
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
5341
|
+
}, [state.verificationTarget, sendEmailCode, sendSmsCode]);
|
|
5342
|
+
const completePasskeyRegistration = react.useCallback(async (credentialId, publicKey) => {
|
|
5343
|
+
const token = await getAccessToken();
|
|
5344
|
+
if (!token) throw new Error("Not authenticated");
|
|
5345
|
+
await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
|
|
5346
|
+
dispatch({ type: "PASSKEY_ACTIVATED", credentialId });
|
|
5347
|
+
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
|
|
5348
|
+
const resolved = resolvePostAuthStep({
|
|
5349
|
+
hasPasskey: true,
|
|
5350
|
+
accounts: state.accounts,
|
|
5351
|
+
persistedMobileFlow: loadMobileFlowState(),
|
|
5352
|
+
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5353
|
+
connectingNewAccount: state.connectingNewAccount
|
|
5354
|
+
});
|
|
5355
|
+
if (resolved.clearPersistedFlow) clearMobileFlowState();
|
|
5356
|
+
dispatch({ type: "NAVIGATE", step: resolved.step });
|
|
5357
|
+
}, [getAccessToken, apiBaseUrl, state.accounts, state.connectingNewAccount]);
|
|
5358
|
+
const handleRegisterPasskey = react.useCallback(async () => {
|
|
5359
|
+
dispatch({ type: "SET_REGISTERING_PASSKEY", value: true });
|
|
5360
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5361
|
+
try {
|
|
5362
|
+
const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
|
|
5363
|
+
const { credentialId, publicKey } = await createPasskeyCredential({
|
|
5364
|
+
userId: user?.id ?? "unknown",
|
|
5365
|
+
displayName: passkeyDisplayName
|
|
5366
|
+
});
|
|
5367
|
+
await completePasskeyRegistration(credentialId, publicKey);
|
|
5368
|
+
} catch (err) {
|
|
5369
|
+
if (err instanceof PasskeyIframeBlockedError) {
|
|
5370
|
+
dispatch({ type: "SET_PASSKEY_POPUP_NEEDED", needed: true });
|
|
5371
|
+
} else {
|
|
5372
|
+
captureException(err);
|
|
5373
|
+
dispatch({
|
|
5374
|
+
type: "SET_ERROR",
|
|
5375
|
+
error: err instanceof Error ? err.message : "Failed to register passkey"
|
|
5376
|
+
});
|
|
4976
5377
|
}
|
|
5378
|
+
} finally {
|
|
5379
|
+
dispatch({ type: "SET_REGISTERING_PASSKEY", value: false });
|
|
5380
|
+
}
|
|
5381
|
+
}, [user, completePasskeyRegistration]);
|
|
5382
|
+
const handleCreatePasskeyViaPopup = react.useCallback(async () => {
|
|
5383
|
+
dispatch({ type: "SET_REGISTERING_PASSKEY", value: true });
|
|
5384
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5385
|
+
try {
|
|
5386
|
+
const token = await getAccessToken();
|
|
5387
|
+
const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
|
|
5388
|
+
const popupOptions = buildPasskeyPopupOptions({
|
|
5389
|
+
userId: user?.id ?? "unknown",
|
|
5390
|
+
displayName: passkeyDisplayName,
|
|
5391
|
+
authToken: token ?? void 0,
|
|
5392
|
+
apiBaseUrl
|
|
5393
|
+
});
|
|
5394
|
+
const { credentialId } = await createPasskeyViaPopup(popupOptions);
|
|
5395
|
+
dispatch({ type: "PASSKEY_ACTIVATED", credentialId });
|
|
5396
|
+
localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
|
|
4977
5397
|
const resolved = resolvePostAuthStep({
|
|
4978
5398
|
hasPasskey: true,
|
|
4979
|
-
accounts:
|
|
4980
|
-
persistedMobileFlow:
|
|
4981
|
-
mobileSetupInProgress:
|
|
4982
|
-
connectingNewAccount:
|
|
5399
|
+
accounts: state.accounts,
|
|
5400
|
+
persistedMobileFlow: loadMobileFlowState(),
|
|
5401
|
+
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5402
|
+
connectingNewAccount: state.connectingNewAccount
|
|
4983
5403
|
});
|
|
4984
|
-
if (resolved.clearPersistedFlow)
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
await handleAuthorizedMobileReturn(existingTransfer, false);
|
|
5012
|
-
return;
|
|
5013
|
-
}
|
|
5014
|
-
if (mobileResolution.kind === "resume-success") {
|
|
5015
|
-
clearMobileFlowState();
|
|
5016
|
-
setMobileFlow(false);
|
|
5017
|
-
setDeeplinkUri(null);
|
|
5018
|
-
setTransfer(existingTransfer);
|
|
5019
|
-
setError(null);
|
|
5020
|
-
setStep("success");
|
|
5021
|
-
onComplete?.(existingTransfer);
|
|
5022
|
-
return;
|
|
5023
|
-
}
|
|
5024
|
-
if (mobileResolution.kind === "resume-failed") {
|
|
5025
|
-
clearMobileFlowState();
|
|
5026
|
-
setMobileFlow(false);
|
|
5027
|
-
setDeeplinkUri(null);
|
|
5028
|
-
setTransfer(existingTransfer);
|
|
5029
|
-
setError("Transfer failed.");
|
|
5030
|
-
setStep("success");
|
|
5031
|
-
return;
|
|
5032
|
-
}
|
|
5033
|
-
if (mobileResolution.kind === "resume-processing") {
|
|
5034
|
-
clearMobileFlowState();
|
|
5035
|
-
setMobileFlow(false);
|
|
5036
|
-
setDeeplinkUri(null);
|
|
5037
|
-
setTransfer(existingTransfer);
|
|
5038
|
-
setError(null);
|
|
5039
|
-
setStep("processing");
|
|
5040
|
-
polling.startPolling(existingTransfer.id);
|
|
5041
|
-
return;
|
|
5042
|
-
}
|
|
5043
|
-
if (mobileResolution.kind === "resume-stale-setup") {
|
|
5044
|
-
clearMobileFlowState();
|
|
5045
|
-
if (!cancelled) setStep("wallet-picker");
|
|
5046
|
-
return;
|
|
5047
|
-
}
|
|
5048
|
-
} catch (err) {
|
|
5049
|
-
if (cancelled) return;
|
|
5050
|
-
enterPersistedMobileFlow(
|
|
5051
|
-
persisted,
|
|
5052
|
-
err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
|
|
5053
|
-
);
|
|
5054
|
-
return;
|
|
5055
|
-
}
|
|
5056
|
-
enterPersistedMobileFlow(persisted);
|
|
5057
|
-
return;
|
|
5058
|
-
}
|
|
5059
|
-
setStep(resolved.step);
|
|
5060
|
-
};
|
|
5061
|
-
const checkPasskey = async () => {
|
|
5062
|
-
try {
|
|
5063
|
-
const token = await getAccessToken();
|
|
5064
|
-
if (!token || cancelled) return;
|
|
5065
|
-
const { config } = await fetchUserConfig(apiBaseUrl, token);
|
|
5066
|
-
if (cancelled) return;
|
|
5067
|
-
if (config.defaultAllowance != null) {
|
|
5068
|
-
setOneTapLimit(config.defaultAllowance);
|
|
5069
|
-
}
|
|
5070
|
-
const allPasskeys = config.passkeys ?? (config.passkey ? [config.passkey] : []);
|
|
5071
|
-
setKnownCredentialIds(allPasskeys.map((p) => p.credentialId));
|
|
5072
|
-
if (allPasskeys.length === 0) {
|
|
5073
|
-
setStep("create-passkey");
|
|
5074
|
-
return;
|
|
5075
|
-
}
|
|
5076
|
-
if (activeCredentialId && allPasskeys.some((p) => p.credentialId === activeCredentialId)) {
|
|
5077
|
-
await restoreOrDeposit(activeCredentialId, token);
|
|
5078
|
-
return;
|
|
5079
|
-
}
|
|
5080
|
-
if (cancelled) return;
|
|
5081
|
-
const credentialIds = allPasskeys.map((p) => p.credentialId);
|
|
5082
|
-
if (isSafari() && isInCrossOriginIframe()) {
|
|
5083
|
-
setStep("verify-passkey");
|
|
5084
|
-
return;
|
|
5085
|
-
}
|
|
5086
|
-
const matched = await findDevicePasskey(credentialIds);
|
|
5087
|
-
if (cancelled) return;
|
|
5088
|
-
if (matched) {
|
|
5089
|
-
setActiveCredentialId(matched);
|
|
5090
|
-
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
|
|
5404
|
+
if (resolved.clearPersistedFlow) clearMobileFlowState();
|
|
5405
|
+
dispatch({ type: "NAVIGATE", step: resolved.step });
|
|
5406
|
+
} catch (err) {
|
|
5407
|
+
captureException(err);
|
|
5408
|
+
dispatch({
|
|
5409
|
+
type: "SET_ERROR",
|
|
5410
|
+
error: err instanceof Error ? err.message : "Failed to register passkey"
|
|
5411
|
+
});
|
|
5412
|
+
} finally {
|
|
5413
|
+
dispatch({ type: "SET_REGISTERING_PASSKEY", value: false });
|
|
5414
|
+
}
|
|
5415
|
+
}, [user, getAccessToken, apiBaseUrl, state.accounts, state.connectingNewAccount]);
|
|
5416
|
+
const handleVerifyPasskeyViaPopup = react.useCallback(async () => {
|
|
5417
|
+
dispatch({ type: "SET_VERIFYING_PASSKEY", value: true });
|
|
5418
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5419
|
+
try {
|
|
5420
|
+
const token = await getAccessToken();
|
|
5421
|
+
const matched = await findDevicePasskeyViaPopup({
|
|
5422
|
+
credentialIds: state.knownCredentialIds,
|
|
5423
|
+
rpId: resolvePasskeyRpId(),
|
|
5424
|
+
authToken: token ?? void 0,
|
|
5425
|
+
apiBaseUrl
|
|
5426
|
+
});
|
|
5427
|
+
if (matched) {
|
|
5428
|
+
dispatch({ type: "PASSKEY_ACTIVATED", credentialId: matched });
|
|
5429
|
+
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
|
|
5430
|
+
if (token) {
|
|
5091
5431
|
reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
|
|
5092
5432
|
});
|
|
5093
|
-
await restoreOrDeposit(matched, token);
|
|
5094
|
-
return;
|
|
5095
5433
|
}
|
|
5096
|
-
|
|
5097
|
-
}
|
|
5098
|
-
|
|
5434
|
+
dispatch({ type: "NAVIGATE", step: "login" });
|
|
5435
|
+
} else {
|
|
5436
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5099
5437
|
}
|
|
5100
|
-
}
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
};
|
|
5105
|
-
}, [
|
|
5106
|
-
ready,
|
|
5107
|
-
authenticated,
|
|
5108
|
-
step,
|
|
5109
|
-
apiBaseUrl,
|
|
5110
|
-
getAccessToken,
|
|
5111
|
-
activeCredentialId,
|
|
5112
|
-
resetHeadlessLogin,
|
|
5113
|
-
enterPersistedMobileFlow,
|
|
5114
|
-
handleAuthorizedMobileReturn,
|
|
5115
|
-
onComplete
|
|
5116
|
-
]);
|
|
5117
|
-
react.useEffect(() => {
|
|
5118
|
-
const loadAction = resolveDataLoadAction({
|
|
5119
|
-
authenticated,
|
|
5120
|
-
step,
|
|
5121
|
-
accountsCount: accounts.length,
|
|
5122
|
-
hasActiveCredential: !!activeCredentialId,
|
|
5123
|
-
loading: loadingDataRef.current
|
|
5124
|
-
});
|
|
5125
|
-
if (loadAction === "reset") {
|
|
5126
|
-
resetDataLoadingState();
|
|
5127
|
-
return;
|
|
5128
|
-
}
|
|
5129
|
-
if (loadAction === "wait") {
|
|
5130
|
-
return;
|
|
5131
|
-
}
|
|
5132
|
-
const credentialId = activeCredentialId;
|
|
5133
|
-
if (!credentialId) {
|
|
5134
|
-
resetDataLoadingState();
|
|
5135
|
-
return;
|
|
5438
|
+
} catch {
|
|
5439
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5440
|
+
} finally {
|
|
5441
|
+
dispatch({ type: "SET_VERIFYING_PASSKEY", value: false });
|
|
5136
5442
|
}
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
const
|
|
5140
|
-
|
|
5141
|
-
|
|
5443
|
+
}, [state.knownCredentialIds, getAccessToken, apiBaseUrl]);
|
|
5444
|
+
const reloadAccounts = react.useCallback(async () => {
|
|
5445
|
+
const token = await getAccessToken();
|
|
5446
|
+
if (!token || !state.activeCredentialId) return;
|
|
5447
|
+
const [accts, prov] = await Promise.all([
|
|
5448
|
+
fetchAccounts(apiBaseUrl, token, state.activeCredentialId),
|
|
5449
|
+
fetchProviders(apiBaseUrl, token)
|
|
5450
|
+
]);
|
|
5451
|
+
const parsedAmt = depositAmount != null ? depositAmount : 0;
|
|
5452
|
+
const defaults = computeSmartDefaults(accts, parsedAmt);
|
|
5453
|
+
dispatch({ type: "ACCOUNTS_RELOADED", accounts: accts, providers: prov, defaults });
|
|
5454
|
+
}, [getAccessToken, state.activeCredentialId, apiBaseUrl, depositAmount]);
|
|
5455
|
+
const handleAuthorizedMobileReturn = react.useCallback(async (authorizedTransfer, isSetup) => {
|
|
5456
|
+
if (handlingMobileReturnRef.current) return;
|
|
5457
|
+
handlingMobileReturnRef.current = true;
|
|
5458
|
+
polling.stopPolling();
|
|
5459
|
+
if (isSetup) {
|
|
5460
|
+
mobileSetupFlowRef.current = false;
|
|
5461
|
+
clearMobileFlowState();
|
|
5142
5462
|
try {
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
fetchProviders(apiBaseUrl, token),
|
|
5147
|
-
fetchAccounts(apiBaseUrl, token, credentialId),
|
|
5148
|
-
fetchChains(apiBaseUrl, token)
|
|
5149
|
-
]);
|
|
5150
|
-
if (cancelled) return;
|
|
5151
|
-
setProviders(prov);
|
|
5152
|
-
setAccounts(accts);
|
|
5153
|
-
setChains(chn);
|
|
5154
|
-
const parsedAmt = depositAmount != null ? depositAmount : 0;
|
|
5155
|
-
const defaults = computeSmartDefaults(accts, parsedAmt);
|
|
5156
|
-
if (defaults) {
|
|
5157
|
-
setSelectedAccountId(defaults.accountId);
|
|
5158
|
-
setSelectedWalletId(defaults.walletId);
|
|
5159
|
-
} else if (prov.length > 0 && !connectingNewAccount) {
|
|
5160
|
-
setSelectedProviderId(prov[0].id);
|
|
5161
|
-
}
|
|
5162
|
-
const persisted = loadMobileFlowState();
|
|
5163
|
-
const resolved = resolvePostAuthStep({
|
|
5164
|
-
hasPasskey: !!activeCredentialId,
|
|
5165
|
-
accounts: accts,
|
|
5166
|
-
persistedMobileFlow: persisted,
|
|
5167
|
-
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5168
|
-
connectingNewAccount
|
|
5169
|
-
});
|
|
5170
|
-
if (resolved.clearPersistedFlow) {
|
|
5171
|
-
clearMobileFlowState();
|
|
5172
|
-
setMobileFlow(false);
|
|
5173
|
-
setDeeplinkUri(null);
|
|
5174
|
-
}
|
|
5175
|
-
const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
|
|
5176
|
-
if (correctableSteps.includes(step)) {
|
|
5177
|
-
setStep(resolved.step);
|
|
5178
|
-
}
|
|
5463
|
+
await reloadAccounts();
|
|
5464
|
+
loadingDataRef.current = false;
|
|
5465
|
+
dispatch({ type: "MOBILE_SETUP_COMPLETE", transfer: authorizedTransfer });
|
|
5179
5466
|
} catch (err) {
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
resetDataLoadingState();
|
|
5187
|
-
}
|
|
5188
|
-
}
|
|
5189
|
-
};
|
|
5190
|
-
load();
|
|
5191
|
-
return () => {
|
|
5192
|
-
cancelled = true;
|
|
5193
|
-
loadingDataRef.current = false;
|
|
5194
|
-
};
|
|
5195
|
-
}, [authenticated, step, accounts.length, apiBaseUrl, getAccessToken, activeCredentialId, depositAmount, connectingNewAccount, resetDataLoadingState]);
|
|
5196
|
-
react.useEffect(() => {
|
|
5197
|
-
if (!polling.transfer) return;
|
|
5198
|
-
if (polling.transfer.status === "COMPLETED") {
|
|
5199
|
-
clearMobileFlowState();
|
|
5200
|
-
setStep("success");
|
|
5201
|
-
setTransfer(polling.transfer);
|
|
5202
|
-
onComplete?.(polling.transfer);
|
|
5203
|
-
} else if (polling.transfer.status === "FAILED") {
|
|
5204
|
-
clearMobileFlowState();
|
|
5205
|
-
setStep("success");
|
|
5206
|
-
setTransfer(polling.transfer);
|
|
5207
|
-
setError("Transfer failed.");
|
|
5208
|
-
}
|
|
5209
|
-
}, [polling.transfer, onComplete]);
|
|
5210
|
-
react.useEffect(() => {
|
|
5211
|
-
if (step !== "processing") {
|
|
5212
|
-
processingStartedAtRef.current = null;
|
|
5213
|
-
return;
|
|
5214
|
-
}
|
|
5215
|
-
if (!processingStartedAtRef.current) {
|
|
5216
|
-
processingStartedAtRef.current = Date.now();
|
|
5217
|
-
}
|
|
5218
|
-
const elapsedMs = Date.now() - processingStartedAtRef.current;
|
|
5219
|
-
const remainingMs = PROCESSING_TIMEOUT_MS - elapsedMs;
|
|
5220
|
-
const handleTimeout = () => {
|
|
5221
|
-
if (!hasProcessingTimedOut(processingStartedAtRef.current, Date.now())) return;
|
|
5222
|
-
const status = getTransferStatus(polling.transfer, transfer);
|
|
5223
|
-
const msg = buildProcessingTimeoutMessage(status);
|
|
5224
|
-
captureException(new Error(msg));
|
|
5225
|
-
polling.stopPolling();
|
|
5226
|
-
setStep("deposit");
|
|
5227
|
-
setError(msg);
|
|
5228
|
-
onError?.(msg);
|
|
5229
|
-
};
|
|
5230
|
-
if (remainingMs <= 0) {
|
|
5231
|
-
handleTimeout();
|
|
5232
|
-
return;
|
|
5233
|
-
}
|
|
5234
|
-
const timeoutId = window.setTimeout(handleTimeout, remainingMs);
|
|
5235
|
-
return () => window.clearTimeout(timeoutId);
|
|
5236
|
-
}, [step, polling.transfer, transfer, polling.stopPolling, onError]);
|
|
5237
|
-
react.useEffect(() => {
|
|
5238
|
-
if (!mobileFlow) {
|
|
5239
|
-
handlingMobileReturnRef.current = false;
|
|
5240
|
-
return;
|
|
5241
|
-
}
|
|
5242
|
-
if (handlingMobileReturnRef.current) return;
|
|
5243
|
-
const polledTransfer = polling.transfer;
|
|
5244
|
-
if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
|
|
5245
|
-
void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
|
|
5246
|
-
}, [mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
|
|
5247
|
-
react.useEffect(() => {
|
|
5248
|
-
if (!mobileFlow) return;
|
|
5249
|
-
if (handlingMobileReturnRef.current) return;
|
|
5250
|
-
const transferIdToResume = pollingTransferIdRef.current ?? transfer?.id;
|
|
5251
|
-
if (!transferIdToResume) return;
|
|
5252
|
-
if (!polling.isPolling) polling.startPolling(transferIdToResume);
|
|
5253
|
-
const handleVisibility = () => {
|
|
5254
|
-
if (document.visibilityState === "visible" && !handlingMobileReturnRef.current) {
|
|
5255
|
-
polling.startPolling(transferIdToResume);
|
|
5467
|
+
handlingMobileReturnRef.current = false;
|
|
5468
|
+
dispatch({
|
|
5469
|
+
type: "SET_ERROR",
|
|
5470
|
+
error: err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
|
|
5471
|
+
});
|
|
5472
|
+
dispatch({ type: "NAVIGATE", step: "open-wallet" });
|
|
5256
5473
|
}
|
|
5257
|
-
};
|
|
5258
|
-
document.addEventListener("visibilitychange", handleVisibility);
|
|
5259
|
-
return () => document.removeEventListener("visibilitychange", handleVisibility);
|
|
5260
|
-
}, [mobileFlow, transfer?.id, polling.isPolling, polling.startPolling]);
|
|
5261
|
-
const pendingSelectSourceAction = authExecutor.pendingSelectSource;
|
|
5262
|
-
const selectSourceChoices = react.useMemo(() => {
|
|
5263
|
-
if (!pendingSelectSourceAction) return [];
|
|
5264
|
-
const options = pendingSelectSourceAction.metadata?.options ?? [];
|
|
5265
|
-
return buildSelectSourceChoices(options);
|
|
5266
|
-
}, [pendingSelectSourceAction]);
|
|
5267
|
-
const selectSourceRecommended = react.useMemo(() => {
|
|
5268
|
-
if (!pendingSelectSourceAction) return null;
|
|
5269
|
-
return pendingSelectSourceAction.metadata?.recommended ?? null;
|
|
5270
|
-
}, [pendingSelectSourceAction]);
|
|
5271
|
-
react.useEffect(() => {
|
|
5272
|
-
if (!pendingSelectSourceAction) {
|
|
5273
|
-
initializedSelectSourceActionRef.current = null;
|
|
5274
|
-
setSelectSourceChainName("");
|
|
5275
|
-
setSelectSourceTokenSymbol("");
|
|
5276
5474
|
return;
|
|
5277
5475
|
}
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
} else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
|
|
5286
|
-
setSelectSourceChainName(selectSourceChoices[0].chainName);
|
|
5287
|
-
setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
|
|
5288
|
-
} else {
|
|
5289
|
-
setSelectSourceChainName("Base");
|
|
5290
|
-
setSelectSourceTokenSymbol("USDC");
|
|
5291
|
-
}
|
|
5292
|
-
initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
|
|
5293
|
-
}, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
|
|
5294
|
-
react.useEffect(() => {
|
|
5295
|
-
if (pendingSelectSourceAction && step === "processing") {
|
|
5296
|
-
preSelectSourceStepRef.current = step;
|
|
5297
|
-
setStep("select-source");
|
|
5298
|
-
} else if (!pendingSelectSourceAction && step === "select-source") {
|
|
5299
|
-
setStep(preSelectSourceStepRef.current ?? "processing");
|
|
5300
|
-
preSelectSourceStepRef.current = null;
|
|
5301
|
-
}
|
|
5302
|
-
}, [pendingSelectSourceAction, step]);
|
|
5303
|
-
const handleSelectSourceChainChange = react.useCallback(
|
|
5304
|
-
(chainName) => {
|
|
5305
|
-
setSelectSourceChainName(chainName);
|
|
5306
|
-
const chain = selectSourceChoices.find((c) => c.chainName === chainName);
|
|
5307
|
-
if (!chain || chain.tokens.length === 0) return;
|
|
5308
|
-
const recommendedToken = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
|
|
5309
|
-
const hasRecommended = !!recommendedToken && chain.tokens.some((t) => t.tokenSymbol === recommendedToken);
|
|
5310
|
-
setSelectSourceTokenSymbol(
|
|
5311
|
-
hasRecommended ? recommendedToken : chain.tokens[0].tokenSymbol
|
|
5312
|
-
);
|
|
5313
|
-
},
|
|
5314
|
-
[selectSourceChoices, selectSourceRecommended]
|
|
5315
|
-
);
|
|
5316
|
-
const pendingConnections = react.useMemo(
|
|
5317
|
-
() => accounts.filter(
|
|
5318
|
-
(a) => a.wallets.length > 0 && !a.wallets.some((w) => w.status === "ACTIVE")
|
|
5319
|
-
),
|
|
5320
|
-
[accounts]
|
|
5321
|
-
);
|
|
5322
|
-
const selectedAccount = accounts.find((a) => a.id === selectedAccountId);
|
|
5323
|
-
const selectedWallet = selectedAccount?.wallets.find((w) => w.id === selectedWalletId);
|
|
5324
|
-
const sourceName = selectedAccount?.name ?? selectedWallet?.chain.name ?? "Wallet";
|
|
5325
|
-
const sourceAddress = selectedWallet ? `${selectedWallet.name.slice(0, 6)}...${selectedWallet.name.slice(-4)}` : void 0;
|
|
5326
|
-
const sourceVerified = selectedWallet?.status === "ACTIVE";
|
|
5327
|
-
const maxSourceBalance = react.useMemo(() => {
|
|
5328
|
-
let max = 0;
|
|
5329
|
-
for (const acct of accounts) {
|
|
5330
|
-
for (const wallet of acct.wallets) {
|
|
5331
|
-
for (const source of wallet.sources) {
|
|
5332
|
-
if (source.balance.available.amount > max) {
|
|
5333
|
-
max = source.balance.available.amount;
|
|
5334
|
-
}
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
|
-
return max;
|
|
5339
|
-
}, [accounts]);
|
|
5340
|
-
const tokenCount = react.useMemo(() => {
|
|
5341
|
-
let count = 0;
|
|
5342
|
-
for (const acct of accounts) {
|
|
5343
|
-
for (const wallet of acct.wallets) {
|
|
5344
|
-
count += wallet.sources.length;
|
|
5345
|
-
}
|
|
5346
|
-
}
|
|
5347
|
-
return count;
|
|
5348
|
-
}, [accounts]);
|
|
5349
|
-
const handlePay = react.useCallback(async (depositAmount2, sourceOverrides) => {
|
|
5350
|
-
const parsedAmount = depositAmount2;
|
|
5351
|
-
if (isNaN(parsedAmount) || parsedAmount < MIN_SEND_AMOUNT_USD) {
|
|
5352
|
-
setError(`Minimum amount is $${MIN_SEND_AMOUNT_USD.toFixed(2)}.`);
|
|
5476
|
+
mobileSetupFlowRef.current = false;
|
|
5477
|
+
clearMobileFlowState();
|
|
5478
|
+
dispatch({ type: "MOBILE_SIGN_READY", transfer: authorizedTransfer });
|
|
5479
|
+
}, [polling.stopPolling, reloadAccounts]);
|
|
5480
|
+
const handlePay = react.useCallback(async (payAmount, sourceOverrides) => {
|
|
5481
|
+
if (isNaN(payAmount) || payAmount < MIN_SEND_AMOUNT_USD) {
|
|
5482
|
+
dispatch({ type: "SET_ERROR", error: `Minimum amount is $${MIN_SEND_AMOUNT_USD.toFixed(2)}.` });
|
|
5353
5483
|
return;
|
|
5354
5484
|
}
|
|
5355
5485
|
if (!sourceOverrides?.sourceId && !sourceId) {
|
|
5356
|
-
|
|
5486
|
+
dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
|
|
5357
5487
|
return;
|
|
5358
5488
|
}
|
|
5359
|
-
if (!activeCredentialId) {
|
|
5360
|
-
|
|
5361
|
-
|
|
5489
|
+
if (!state.activeCredentialId) {
|
|
5490
|
+
dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
|
|
5491
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5362
5492
|
return;
|
|
5363
5493
|
}
|
|
5364
5494
|
const isSetupRedirect = mobileSetupFlowRef.current;
|
|
5365
|
-
|
|
5366
|
-
setStep("open-wallet");
|
|
5367
|
-
} else {
|
|
5368
|
-
setStep("processing");
|
|
5369
|
-
}
|
|
5495
|
+
dispatch({ type: "PAY_STARTED", isSetupRedirect });
|
|
5370
5496
|
processingStartedAtRef.current = Date.now();
|
|
5371
|
-
setError(null);
|
|
5372
|
-
setCreatingTransfer(true);
|
|
5373
|
-
setDeeplinkUri(null);
|
|
5374
|
-
setMobileFlow(false);
|
|
5375
5497
|
try {
|
|
5376
|
-
if (transfer?.status === "AUTHORIZED") {
|
|
5377
|
-
const signedTransfer2 = await transferSigning.signTransfer(transfer.id);
|
|
5378
|
-
|
|
5379
|
-
polling.startPolling(transfer.id);
|
|
5498
|
+
if (state.transfer?.status === "AUTHORIZED") {
|
|
5499
|
+
const signedTransfer2 = await transferSigning.signTransfer(state.transfer.id);
|
|
5500
|
+
dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer2 });
|
|
5501
|
+
polling.startPolling(state.transfer.id);
|
|
5380
5502
|
return;
|
|
5381
5503
|
}
|
|
5382
5504
|
const token = await getAccessToken();
|
|
@@ -5384,21 +5506,21 @@ function SwypePaymentInner({
|
|
|
5384
5506
|
let effectiveSourceType = sourceOverrides?.sourceType ?? sourceType;
|
|
5385
5507
|
let effectiveSourceId = sourceOverrides?.sourceId ?? sourceId;
|
|
5386
5508
|
if (effectiveSourceType === "accountId") {
|
|
5387
|
-
const acct = accounts.find((a) => a.id === effectiveSourceId);
|
|
5509
|
+
const acct = state.accounts.find((a) => a.id === effectiveSourceId);
|
|
5388
5510
|
const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
|
|
5389
5511
|
if (activeWallet) {
|
|
5390
5512
|
effectiveSourceType = "walletId";
|
|
5391
5513
|
effectiveSourceId = activeWallet.id;
|
|
5392
5514
|
}
|
|
5393
5515
|
}
|
|
5394
|
-
const isActiveWallet = effectiveSourceType === "walletId" && accounts.some(
|
|
5516
|
+
const isActiveWallet = effectiveSourceType === "walletId" && state.accounts.some(
|
|
5395
5517
|
(a) => a.wallets.some((w) => w.id === effectiveSourceId && w.status === "ACTIVE")
|
|
5396
5518
|
);
|
|
5397
5519
|
if (!isActiveWallet && !isSetupRedirect) {
|
|
5398
5520
|
let found = false;
|
|
5399
|
-
for (const acct of accounts) {
|
|
5521
|
+
for (const acct of state.accounts) {
|
|
5400
5522
|
for (const wallet of acct.wallets) {
|
|
5401
|
-
if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >=
|
|
5523
|
+
if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >= payAmount)) {
|
|
5402
5524
|
effectiveSourceType = "walletId";
|
|
5403
5525
|
effectiveSourceId = wallet.id;
|
|
5404
5526
|
found = true;
|
|
@@ -5410,30 +5532,28 @@ function SwypePaymentInner({
|
|
|
5410
5532
|
}
|
|
5411
5533
|
const t = await createTransfer(apiBaseUrl, token, {
|
|
5412
5534
|
id: idempotencyKey,
|
|
5413
|
-
credentialId: activeCredentialId,
|
|
5535
|
+
credentialId: state.activeCredentialId,
|
|
5414
5536
|
merchantAuthorization,
|
|
5415
5537
|
sourceType: effectiveSourceType,
|
|
5416
5538
|
sourceId: effectiveSourceId,
|
|
5417
5539
|
destination,
|
|
5418
|
-
amount:
|
|
5540
|
+
amount: payAmount
|
|
5419
5541
|
});
|
|
5420
|
-
|
|
5542
|
+
dispatch({ type: "TRANSFER_CREATED", transfer: t });
|
|
5421
5543
|
if (t.authorizationSessions && t.authorizationSessions.length > 0) {
|
|
5422
|
-
const
|
|
5423
|
-
useWalletConnector,
|
|
5544
|
+
const useConnector = shouldUseWalletConnector({
|
|
5545
|
+
useWalletConnector: useWalletConnectorProp,
|
|
5424
5546
|
userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
|
|
5425
5547
|
});
|
|
5426
|
-
if (!
|
|
5548
|
+
if (!useConnector) {
|
|
5427
5549
|
const uri = t.authorizationSessions[0].uri;
|
|
5428
|
-
setMobileFlow(true);
|
|
5429
5550
|
pollingTransferIdRef.current = t.id;
|
|
5430
5551
|
polling.startPolling(t.id);
|
|
5431
|
-
|
|
5432
|
-
setStep("open-wallet");
|
|
5552
|
+
dispatch({ type: "MOBILE_DEEPLINK_READY", deeplinkUri: uri });
|
|
5433
5553
|
persistMobileFlowState({
|
|
5434
5554
|
transferId: t.id,
|
|
5435
5555
|
deeplinkUri: uri,
|
|
5436
|
-
providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : selectedProviderId,
|
|
5556
|
+
providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : state.selectedProviderId,
|
|
5437
5557
|
isSetup: mobileSetupFlowRef.current
|
|
5438
5558
|
});
|
|
5439
5559
|
triggerDeeplink(uri);
|
|
@@ -5443,55 +5563,58 @@ function SwypePaymentInner({
|
|
|
5443
5563
|
}
|
|
5444
5564
|
}
|
|
5445
5565
|
const signedTransfer = await transferSigning.signTransfer(t.id);
|
|
5446
|
-
|
|
5566
|
+
dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer });
|
|
5447
5567
|
polling.startPolling(t.id);
|
|
5448
5568
|
} catch (err) {
|
|
5449
5569
|
captureException(err);
|
|
5450
5570
|
const msg = err instanceof Error ? err.message : "Transfer failed";
|
|
5451
|
-
|
|
5571
|
+
dispatch({
|
|
5572
|
+
type: "PAY_ERROR",
|
|
5573
|
+
error: msg,
|
|
5574
|
+
fallbackStep: isSetupRedirect ? "wallet-picker" : "deposit"
|
|
5575
|
+
});
|
|
5452
5576
|
onError?.(msg);
|
|
5453
|
-
setStep(isSetupRedirect ? "wallet-picker" : "deposit");
|
|
5454
5577
|
} finally {
|
|
5455
|
-
|
|
5578
|
+
dispatch({ type: "PAY_ENDED" });
|
|
5456
5579
|
}
|
|
5457
5580
|
}, [
|
|
5458
5581
|
sourceId,
|
|
5459
5582
|
sourceType,
|
|
5460
|
-
activeCredentialId,
|
|
5583
|
+
state.activeCredentialId,
|
|
5584
|
+
state.transfer,
|
|
5585
|
+
state.accounts,
|
|
5586
|
+
state.selectedProviderId,
|
|
5461
5587
|
destination,
|
|
5462
5588
|
apiBaseUrl,
|
|
5463
5589
|
getAccessToken,
|
|
5464
|
-
accounts,
|
|
5465
5590
|
authExecutor,
|
|
5466
5591
|
transferSigning,
|
|
5467
5592
|
polling,
|
|
5468
5593
|
onError,
|
|
5469
|
-
|
|
5594
|
+
useWalletConnectorProp,
|
|
5470
5595
|
idempotencyKey,
|
|
5471
|
-
merchantAuthorization
|
|
5472
|
-
transfer
|
|
5596
|
+
merchantAuthorization
|
|
5473
5597
|
]);
|
|
5474
|
-
const [increasingLimit, setIncreasingLimit] = react.useState(false);
|
|
5475
5598
|
const handleIncreaseLimit = react.useCallback(async () => {
|
|
5476
5599
|
const parsedAmount = depositAmount ?? 5;
|
|
5477
5600
|
if (!sourceId) {
|
|
5478
|
-
|
|
5601
|
+
dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
|
|
5479
5602
|
return;
|
|
5480
5603
|
}
|
|
5481
|
-
if (!activeCredentialId) {
|
|
5482
|
-
|
|
5483
|
-
|
|
5604
|
+
if (!state.activeCredentialId) {
|
|
5605
|
+
dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
|
|
5606
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5484
5607
|
return;
|
|
5485
5608
|
}
|
|
5486
|
-
|
|
5487
|
-
|
|
5609
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5610
|
+
dispatch({ type: "SET_INCREASING_LIMIT", value: true });
|
|
5488
5611
|
try {
|
|
5489
5612
|
const token = await getAccessToken();
|
|
5490
5613
|
if (!token) throw new Error("Not authenticated");
|
|
5491
5614
|
let effectiveSourceType = sourceType;
|
|
5492
5615
|
let effectiveSourceId = sourceId;
|
|
5493
5616
|
if (effectiveSourceType === "accountId") {
|
|
5494
|
-
const acct = accounts.find((a) => a.id === effectiveSourceId);
|
|
5617
|
+
const acct = state.accounts.find((a) => a.id === effectiveSourceId);
|
|
5495
5618
|
const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
|
|
5496
5619
|
if (activeWallet) {
|
|
5497
5620
|
effectiveSourceType = "walletId";
|
|
@@ -5500,188 +5623,118 @@ function SwypePaymentInner({
|
|
|
5500
5623
|
}
|
|
5501
5624
|
const t = await createTransfer(apiBaseUrl, token, {
|
|
5502
5625
|
id: idempotencyKey,
|
|
5503
|
-
credentialId: activeCredentialId,
|
|
5626
|
+
credentialId: state.activeCredentialId,
|
|
5504
5627
|
merchantAuthorization,
|
|
5505
5628
|
sourceType: effectiveSourceType,
|
|
5506
5629
|
sourceId: effectiveSourceId,
|
|
5507
5630
|
destination,
|
|
5508
5631
|
amount: parsedAmount
|
|
5509
5632
|
});
|
|
5510
|
-
setTransfer(t);
|
|
5511
5633
|
if (t.authorizationSessions && t.authorizationSessions.length > 0) {
|
|
5512
5634
|
const uri = t.authorizationSessions[0].uri;
|
|
5513
|
-
setMobileFlow(true);
|
|
5514
5635
|
pollingTransferIdRef.current = t.id;
|
|
5515
5636
|
mobileSetupFlowRef.current = true;
|
|
5516
5637
|
handlingMobileReturnRef.current = false;
|
|
5517
5638
|
polling.startPolling(t.id);
|
|
5518
|
-
|
|
5639
|
+
dispatch({ type: "INCREASE_LIMIT_DEEPLINK", transfer: t, deeplinkUri: uri });
|
|
5519
5640
|
persistMobileFlowState({
|
|
5520
5641
|
transferId: t.id,
|
|
5521
5642
|
deeplinkUri: uri,
|
|
5522
|
-
providerId: selectedProviderId,
|
|
5643
|
+
providerId: state.selectedProviderId,
|
|
5523
5644
|
isSetup: true
|
|
5524
5645
|
});
|
|
5525
5646
|
triggerDeeplink(uri);
|
|
5647
|
+
} else {
|
|
5648
|
+
dispatch({ type: "TRANSFER_CREATED", transfer: t });
|
|
5526
5649
|
}
|
|
5527
5650
|
} catch (err) {
|
|
5528
5651
|
captureException(err);
|
|
5529
5652
|
const msg = err instanceof Error ? err.message : "Failed to increase limit";
|
|
5530
|
-
|
|
5653
|
+
dispatch({ type: "SET_ERROR", error: msg });
|
|
5531
5654
|
onError?.(msg);
|
|
5532
5655
|
} finally {
|
|
5533
|
-
|
|
5656
|
+
dispatch({ type: "SET_INCREASING_LIMIT", value: false });
|
|
5534
5657
|
}
|
|
5535
5658
|
}, [
|
|
5536
5659
|
depositAmount,
|
|
5537
5660
|
sourceId,
|
|
5538
5661
|
sourceType,
|
|
5539
|
-
activeCredentialId,
|
|
5662
|
+
state.activeCredentialId,
|
|
5663
|
+
state.accounts,
|
|
5664
|
+
state.selectedProviderId,
|
|
5540
5665
|
apiBaseUrl,
|
|
5541
5666
|
getAccessToken,
|
|
5542
|
-
accounts,
|
|
5543
5667
|
polling,
|
|
5544
5668
|
onError,
|
|
5545
5669
|
idempotencyKey,
|
|
5546
5670
|
merchantAuthorization,
|
|
5547
|
-
destination
|
|
5548
|
-
selectedProviderId
|
|
5671
|
+
destination
|
|
5549
5672
|
]);
|
|
5550
|
-
const
|
|
5551
|
-
const
|
|
5552
|
-
if (!
|
|
5553
|
-
await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
|
|
5554
|
-
setActiveCredentialId(credentialId);
|
|
5555
|
-
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
|
|
5556
|
-
setPasskeyPopupNeeded(false);
|
|
5557
|
-
const resolved = resolvePostAuthStep({
|
|
5558
|
-
hasPasskey: true,
|
|
5559
|
-
accounts,
|
|
5560
|
-
persistedMobileFlow: loadMobileFlowState(),
|
|
5561
|
-
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5562
|
-
connectingNewAccount
|
|
5563
|
-
});
|
|
5564
|
-
if (resolved.clearPersistedFlow) {
|
|
5565
|
-
clearMobileFlowState();
|
|
5566
|
-
}
|
|
5567
|
-
setStep(resolved.step);
|
|
5568
|
-
}, [getAccessToken, apiBaseUrl, accounts, connectingNewAccount]);
|
|
5569
|
-
const handleRegisterPasskey = react.useCallback(async () => {
|
|
5570
|
-
setRegisteringPasskey(true);
|
|
5571
|
-
setError(null);
|
|
5572
|
-
try {
|
|
5573
|
-
const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
|
|
5574
|
-
const { credentialId, publicKey } = await createPasskeyCredential({
|
|
5575
|
-
userId: user?.id ?? "unknown",
|
|
5576
|
-
displayName: passkeyDisplayName
|
|
5577
|
-
});
|
|
5578
|
-
await completePasskeyRegistration(credentialId, publicKey);
|
|
5579
|
-
} catch (err) {
|
|
5580
|
-
if (err instanceof PasskeyIframeBlockedError) {
|
|
5581
|
-
setPasskeyPopupNeeded(true);
|
|
5582
|
-
} else {
|
|
5583
|
-
captureException(err);
|
|
5584
|
-
setError(err instanceof Error ? err.message : "Failed to register passkey");
|
|
5585
|
-
}
|
|
5586
|
-
} finally {
|
|
5587
|
-
setRegisteringPasskey(false);
|
|
5588
|
-
}
|
|
5589
|
-
}, [user, completePasskeyRegistration]);
|
|
5590
|
-
const handleCreatePasskeyViaPopup = react.useCallback(async () => {
|
|
5591
|
-
setRegisteringPasskey(true);
|
|
5592
|
-
setError(null);
|
|
5673
|
+
const handleConfirmSign = react.useCallback(async () => {
|
|
5674
|
+
const t = state.transfer ?? polling.transfer;
|
|
5675
|
+
if (!t) return;
|
|
5593
5676
|
try {
|
|
5594
|
-
const
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
displayName: passkeyDisplayName,
|
|
5599
|
-
authToken: token ?? void 0,
|
|
5600
|
-
apiBaseUrl
|
|
5601
|
-
});
|
|
5602
|
-
const { credentialId, publicKey } = await createPasskeyViaPopup(
|
|
5603
|
-
popupOptions,
|
|
5604
|
-
knownCredentialIds
|
|
5605
|
-
);
|
|
5606
|
-
await completePasskeyRegistration(credentialId, publicKey);
|
|
5677
|
+
const signedTransfer = await transferSigning.signTransfer(t.id);
|
|
5678
|
+
clearMobileFlowState();
|
|
5679
|
+
dispatch({ type: "CONFIRM_SIGN_SUCCESS", transfer: signedTransfer });
|
|
5680
|
+
polling.startPolling(t.id);
|
|
5607
5681
|
} catch (err) {
|
|
5608
5682
|
captureException(err);
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5683
|
+
const msg = err instanceof Error ? err.message : "Failed to sign transfer";
|
|
5684
|
+
dispatch({ type: "SET_ERROR", error: msg });
|
|
5685
|
+
onError?.(msg);
|
|
5612
5686
|
}
|
|
5613
|
-
}, [
|
|
5614
|
-
const
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
if (matched) {
|
|
5626
|
-
setActiveCredentialId(matched);
|
|
5627
|
-
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
|
|
5628
|
-
if (token) {
|
|
5629
|
-
reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
|
|
5630
|
-
});
|
|
5631
|
-
}
|
|
5632
|
-
setStep("login");
|
|
5633
|
-
} else {
|
|
5634
|
-
setStep("create-passkey");
|
|
5635
|
-
}
|
|
5636
|
-
} catch {
|
|
5637
|
-
setStep("create-passkey");
|
|
5638
|
-
} finally {
|
|
5639
|
-
setVerifyingPasskeyPopup(false);
|
|
5687
|
+
}, [state.transfer, polling.transfer, polling.startPolling, transferSigning, onError]);
|
|
5688
|
+
const handleRetryMobileStatus = react.useCallback(() => {
|
|
5689
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5690
|
+
handlingMobileReturnRef.current = false;
|
|
5691
|
+
const currentTransfer = polling.transfer ?? state.transfer;
|
|
5692
|
+
if (currentTransfer?.status === "AUTHORIZED") {
|
|
5693
|
+
void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
|
|
5697
|
+
if (transferIdToResume) {
|
|
5698
|
+
polling.startPolling(transferIdToResume);
|
|
5640
5699
|
}
|
|
5641
|
-
}, [
|
|
5700
|
+
}, [handleAuthorizedMobileReturn, polling, state.transfer]);
|
|
5642
5701
|
const handleSelectProvider = react.useCallback((providerId) => {
|
|
5643
|
-
|
|
5644
|
-
setSelectedAccountId(null);
|
|
5645
|
-
setConnectingNewAccount(true);
|
|
5702
|
+
dispatch({ type: "SELECT_PROVIDER", providerId });
|
|
5646
5703
|
const isMobile = !shouldUseWalletConnector({
|
|
5647
|
-
useWalletConnector,
|
|
5704
|
+
useWalletConnector: useWalletConnectorProp,
|
|
5648
5705
|
userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
|
|
5649
5706
|
});
|
|
5650
5707
|
if (isMobile) {
|
|
5651
5708
|
handlingMobileReturnRef.current = false;
|
|
5652
5709
|
mobileSetupFlowRef.current = true;
|
|
5653
|
-
const
|
|
5654
|
-
handlePay(
|
|
5710
|
+
const amount = depositAmount ?? 5;
|
|
5711
|
+
handlePay(amount, { sourceType: "providerId", sourceId: providerId });
|
|
5655
5712
|
} else {
|
|
5656
|
-
|
|
5713
|
+
dispatch({ type: "NAVIGATE", step: "deposit" });
|
|
5657
5714
|
}
|
|
5658
|
-
}, [
|
|
5715
|
+
}, [useWalletConnectorProp, depositAmount, handlePay]);
|
|
5659
5716
|
const handleContinueConnection = react.useCallback(
|
|
5660
5717
|
(accountId) => {
|
|
5661
|
-
const acct = accounts.find((a) => a.id === accountId);
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5718
|
+
const acct = state.accounts.find((a) => a.id === accountId);
|
|
5719
|
+
dispatch({
|
|
5720
|
+
type: "SELECT_ACCOUNT",
|
|
5721
|
+
accountId,
|
|
5722
|
+
walletId: acct?.wallets[0]?.id ?? null
|
|
5723
|
+
});
|
|
5666
5724
|
},
|
|
5667
|
-
[accounts]
|
|
5725
|
+
[state.accounts]
|
|
5668
5726
|
);
|
|
5669
5727
|
const handleNewPayment = react.useCallback(() => {
|
|
5670
5728
|
clearMobileFlowState();
|
|
5671
|
-
setStep("deposit");
|
|
5672
|
-
setTransfer(null);
|
|
5673
|
-
setError(null);
|
|
5674
|
-
setAmount(depositAmount != null ? depositAmount.toString() : "");
|
|
5675
|
-
setMobileFlow(false);
|
|
5676
|
-
setDeeplinkUri(null);
|
|
5677
5729
|
processingStartedAtRef.current = null;
|
|
5678
5730
|
pollingTransferIdRef.current = null;
|
|
5679
|
-
mobileSigningTransferIdRef.current = null;
|
|
5680
5731
|
preSelectSourceStepRef.current = null;
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5732
|
+
dispatch({
|
|
5733
|
+
type: "NEW_PAYMENT",
|
|
5734
|
+
depositAmount,
|
|
5735
|
+
firstAccountId: state.accounts.length > 0 ? state.accounts[0].id : null
|
|
5736
|
+
});
|
|
5737
|
+
}, [depositAmount, state.accounts]);
|
|
5685
5738
|
const handleLogout = react.useCallback(async () => {
|
|
5686
5739
|
try {
|
|
5687
5740
|
await logout();
|
|
@@ -5692,254 +5745,464 @@ function SwypePaymentInner({
|
|
|
5692
5745
|
window.localStorage.removeItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
|
|
5693
5746
|
}
|
|
5694
5747
|
polling.stopPolling();
|
|
5695
|
-
setActiveCredentialId(null);
|
|
5696
|
-
setStep("login");
|
|
5697
|
-
setError(null);
|
|
5698
|
-
setTransfer(null);
|
|
5699
|
-
setCreatingTransfer(false);
|
|
5700
|
-
setRegisteringPasskey(false);
|
|
5701
|
-
setProviders([]);
|
|
5702
|
-
setAccounts([]);
|
|
5703
|
-
setChains([]);
|
|
5704
|
-
setSelectedAccountId(null);
|
|
5705
|
-
setSelectedWalletId(null);
|
|
5706
|
-
setSelectedProviderId(null);
|
|
5707
|
-
setConnectingNewAccount(false);
|
|
5708
|
-
setAmount(depositAmount != null ? depositAmount.toString() : "");
|
|
5709
|
-
setMobileFlow(false);
|
|
5710
|
-
setDeeplinkUri(null);
|
|
5711
5748
|
preSelectSourceStepRef.current = null;
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5749
|
+
setAuthInput("");
|
|
5750
|
+
setOtpCode("");
|
|
5751
|
+
dispatch({ type: "LOGOUT", depositAmount });
|
|
5752
|
+
}, [logout, polling, depositAmount]);
|
|
5753
|
+
const pendingSelectSourceAction = authExecutor.pendingSelectSource;
|
|
5754
|
+
const selectSourceChoices = react.useMemo(() => {
|
|
5755
|
+
if (!pendingSelectSourceAction) return [];
|
|
5756
|
+
const options = pendingSelectSourceAction.metadata?.options ?? [];
|
|
5757
|
+
return buildSelectSourceChoices(options);
|
|
5758
|
+
}, [pendingSelectSourceAction]);
|
|
5759
|
+
const selectSourceRecommended = react.useMemo(() => {
|
|
5760
|
+
if (!pendingSelectSourceAction) return null;
|
|
5761
|
+
return pendingSelectSourceAction.metadata?.recommended ?? null;
|
|
5762
|
+
}, [pendingSelectSourceAction]);
|
|
5763
|
+
const handleSelectSourceChainChange = react.useCallback(
|
|
5764
|
+
(chainName) => {
|
|
5765
|
+
setSelectSourceChainName(chainName);
|
|
5766
|
+
const chain = selectSourceChoices.find((c) => c.chainName === chainName);
|
|
5767
|
+
if (!chain || chain.tokens.length === 0) return;
|
|
5768
|
+
const recommendedToken = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
|
|
5769
|
+
const hasRecommended = !!recommendedToken && chain.tokens.some((t) => t.tokenSymbol === recommendedToken);
|
|
5770
|
+
setSelectSourceTokenSymbol(
|
|
5771
|
+
hasRecommended ? recommendedToken : chain.tokens[0].tokenSymbol
|
|
5772
|
+
);
|
|
5773
|
+
},
|
|
5774
|
+
[selectSourceChoices, selectSourceRecommended]
|
|
5775
|
+
);
|
|
5776
|
+
const handleConfirmSelectSource = react.useCallback(() => {
|
|
5777
|
+
authExecutor.resolveSelectSource({
|
|
5778
|
+
chainName: selectSourceChainName,
|
|
5779
|
+
tokenSymbol: selectSourceTokenSymbol
|
|
5780
|
+
});
|
|
5781
|
+
}, [authExecutor, selectSourceChainName, selectSourceTokenSymbol]);
|
|
5782
|
+
react.useEffect(() => {
|
|
5783
|
+
if (depositAmount != null) {
|
|
5784
|
+
dispatch({ type: "SYNC_AMOUNT", amount: depositAmount.toString() });
|
|
5728
5785
|
}
|
|
5729
|
-
}, [
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
);
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
onOtpChange: (code) => {
|
|
5754
|
-
setOtpCode(code);
|
|
5755
|
-
setError(null);
|
|
5756
|
-
},
|
|
5757
|
-
onVerify: handleVerifyLoginCode,
|
|
5758
|
-
onResend: handleResendLoginCode,
|
|
5759
|
-
onBack: () => {
|
|
5760
|
-
setVerificationTarget(null);
|
|
5761
|
-
setOtpCode("");
|
|
5762
|
-
setError(null);
|
|
5763
|
-
setStep("login");
|
|
5764
|
-
},
|
|
5765
|
-
verifying: activeOtpStatus === "submitting-code",
|
|
5766
|
-
error
|
|
5767
|
-
}
|
|
5768
|
-
);
|
|
5769
|
-
}
|
|
5770
|
-
if ((step === "login" || step === "otp-verify") && authenticated) {
|
|
5771
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label: "Verifying your passkey..." }) }) });
|
|
5772
|
-
}
|
|
5773
|
-
if (step === "verify-passkey") {
|
|
5774
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5775
|
-
VerifyPasskeyScreen,
|
|
5776
|
-
{
|
|
5777
|
-
onVerify: handleVerifyPasskeyViaPopup,
|
|
5778
|
-
onBack: handleLogout,
|
|
5779
|
-
verifying: verifyingPasskeyPopup,
|
|
5780
|
-
error
|
|
5781
|
-
}
|
|
5782
|
-
);
|
|
5783
|
-
}
|
|
5784
|
-
if (step === "create-passkey") {
|
|
5785
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5786
|
-
CreatePasskeyScreen,
|
|
5787
|
-
{
|
|
5788
|
-
onCreatePasskey: handleRegisterPasskey,
|
|
5789
|
-
onBack: handleLogout,
|
|
5790
|
-
creating: registeringPasskey,
|
|
5791
|
-
error,
|
|
5792
|
-
popupFallback: passkeyPopupNeeded,
|
|
5793
|
-
onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup
|
|
5786
|
+
}, [depositAmount]);
|
|
5787
|
+
react.useEffect(() => {
|
|
5788
|
+
if (authenticated) return;
|
|
5789
|
+
if (activeOtpErrorMessage) dispatch({ type: "SET_ERROR", error: activeOtpErrorMessage });
|
|
5790
|
+
}, [activeOtpErrorMessage, authenticated]);
|
|
5791
|
+
react.useEffect(() => {
|
|
5792
|
+
if (state.step === "otp-verify" && /^\d{6}$/.test(otpCode.trim()) && activeOtpStatus !== "submitting-code") {
|
|
5793
|
+
handleVerifyLoginCode();
|
|
5794
|
+
}
|
|
5795
|
+
}, [otpCode, state.step, activeOtpStatus, handleVerifyLoginCode]);
|
|
5796
|
+
react.useEffect(() => {
|
|
5797
|
+
if (!ready || !authenticated) return;
|
|
5798
|
+
if (state.step !== "login" && state.step !== "otp-verify") return;
|
|
5799
|
+
let cancelled = false;
|
|
5800
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
5801
|
+
setAuthInput("");
|
|
5802
|
+
setOtpCode("");
|
|
5803
|
+
const restoreOrDeposit = async (credId, token) => {
|
|
5804
|
+
const persisted = loadMobileFlowState();
|
|
5805
|
+
let accts = [];
|
|
5806
|
+
try {
|
|
5807
|
+
accts = await fetchAccounts(apiBaseUrl, token, credId);
|
|
5808
|
+
if (cancelled) return;
|
|
5809
|
+
} catch {
|
|
5794
5810
|
}
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5811
|
+
const resolved = resolvePostAuthStep({
|
|
5812
|
+
hasPasskey: true,
|
|
5813
|
+
accounts: accts,
|
|
5814
|
+
persistedMobileFlow: persisted,
|
|
5815
|
+
mobileSetupInProgress: false,
|
|
5816
|
+
connectingNewAccount: false
|
|
5817
|
+
});
|
|
5818
|
+
if (resolved.clearPersistedFlow) clearMobileFlowState();
|
|
5819
|
+
if (resolved.step === "deposit" && persisted && persisted.isSetup) {
|
|
5820
|
+
try {
|
|
5821
|
+
const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
|
|
5822
|
+
if (cancelled) return;
|
|
5823
|
+
if (existingTransfer.status === "AUTHORIZED") {
|
|
5824
|
+
await handleAuthorizedMobileReturn(existingTransfer, true);
|
|
5825
|
+
return;
|
|
5826
|
+
}
|
|
5827
|
+
} catch {
|
|
5828
|
+
}
|
|
5807
5829
|
}
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5830
|
+
if (resolved.step === "open-wallet" && persisted) {
|
|
5831
|
+
try {
|
|
5832
|
+
const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
|
|
5833
|
+
if (cancelled) return;
|
|
5834
|
+
const mobileResolution = resolveRestoredMobileFlow(
|
|
5835
|
+
existingTransfer.status,
|
|
5836
|
+
persisted.isSetup
|
|
5837
|
+
);
|
|
5838
|
+
if (mobileResolution.kind === "resume-setup-deposit") {
|
|
5839
|
+
await handleAuthorizedMobileReturn(existingTransfer, true);
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
if (mobileResolution.kind === "resume-confirm-sign") {
|
|
5843
|
+
await handleAuthorizedMobileReturn(existingTransfer, false);
|
|
5844
|
+
return;
|
|
5845
|
+
}
|
|
5846
|
+
if (mobileResolution.kind === "resume-success") {
|
|
5847
|
+
clearMobileFlowState();
|
|
5848
|
+
dispatch({ type: "MOBILE_RESUME_SUCCESS", transfer: existingTransfer });
|
|
5849
|
+
onComplete?.(existingTransfer);
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
if (mobileResolution.kind === "resume-failed") {
|
|
5853
|
+
clearMobileFlowState();
|
|
5854
|
+
dispatch({ type: "MOBILE_RESUME_FAILED", transfer: existingTransfer });
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
if (mobileResolution.kind === "resume-processing") {
|
|
5858
|
+
clearMobileFlowState();
|
|
5859
|
+
dispatch({ type: "MOBILE_RESUME_PROCESSING", transfer: existingTransfer });
|
|
5860
|
+
polling.startPolling(existingTransfer.id);
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
if (mobileResolution.kind === "resume-stale-setup") {
|
|
5864
|
+
clearMobileFlowState();
|
|
5865
|
+
if (!cancelled) dispatch({ type: "NAVIGATE", step: "wallet-picker" });
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
} catch (err) {
|
|
5869
|
+
if (cancelled) return;
|
|
5870
|
+
dispatch({
|
|
5871
|
+
type: "ENTER_MOBILE_FLOW",
|
|
5872
|
+
deeplinkUri: persisted.deeplinkUri,
|
|
5873
|
+
providerId: persisted.providerId,
|
|
5874
|
+
error: err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
|
|
5875
|
+
});
|
|
5876
|
+
pollingTransferIdRef.current = persisted.transferId;
|
|
5877
|
+
mobileSetupFlowRef.current = persisted.isSetup;
|
|
5878
|
+
polling.startPolling(persisted.transferId);
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5881
|
+
dispatch({
|
|
5882
|
+
type: "ENTER_MOBILE_FLOW",
|
|
5883
|
+
deeplinkUri: persisted.deeplinkUri,
|
|
5884
|
+
providerId: persisted.providerId
|
|
5885
|
+
});
|
|
5886
|
+
pollingTransferIdRef.current = persisted.transferId;
|
|
5887
|
+
mobileSetupFlowRef.current = persisted.isSetup;
|
|
5888
|
+
polling.startPolling(persisted.transferId);
|
|
5889
|
+
return;
|
|
5821
5890
|
}
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5891
|
+
dispatch({ type: "NAVIGATE", step: resolved.step });
|
|
5892
|
+
};
|
|
5893
|
+
const checkPasskey = async () => {
|
|
5894
|
+
try {
|
|
5895
|
+
const token = await getAccessToken();
|
|
5896
|
+
if (!token || cancelled) return;
|
|
5897
|
+
const { config } = await fetchUserConfig(apiBaseUrl, token);
|
|
5898
|
+
if (cancelled) return;
|
|
5899
|
+
const allPasskeys = config.passkeys ?? (config.passkey ? [config.passkey] : []);
|
|
5900
|
+
dispatch({
|
|
5901
|
+
type: "PASSKEY_CONFIG_LOADED",
|
|
5902
|
+
knownIds: allPasskeys.map((p) => p.credentialId),
|
|
5903
|
+
oneTapLimit: config.defaultAllowance ?? void 0
|
|
5904
|
+
});
|
|
5905
|
+
if (allPasskeys.length === 0) {
|
|
5906
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5907
|
+
return;
|
|
5908
|
+
}
|
|
5909
|
+
if (state.activeCredentialId && allPasskeys.some((p) => p.credentialId === state.activeCredentialId)) {
|
|
5910
|
+
await restoreOrDeposit(state.activeCredentialId, token);
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
if (cancelled) return;
|
|
5914
|
+
if (isSafari() && isInCrossOriginIframe()) {
|
|
5915
|
+
dispatch({ type: "NAVIGATE", step: "verify-passkey" });
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5918
|
+
const credentialIds = allPasskeys.map((p) => p.credentialId);
|
|
5919
|
+
const matched = await findDevicePasskey(credentialIds);
|
|
5920
|
+
if (cancelled) return;
|
|
5921
|
+
if (matched) {
|
|
5922
|
+
dispatch({ type: "PASSKEY_ACTIVATED", credentialId: matched });
|
|
5923
|
+
window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
|
|
5924
|
+
reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
|
|
5925
|
+
});
|
|
5926
|
+
await restoreOrDeposit(matched, token);
|
|
5927
|
+
return;
|
|
5928
|
+
}
|
|
5929
|
+
dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5930
|
+
} catch {
|
|
5931
|
+
if (!cancelled) dispatch({ type: "NAVIGATE", step: "create-passkey" });
|
|
5834
5932
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5933
|
+
};
|
|
5934
|
+
checkPasskey();
|
|
5935
|
+
return () => {
|
|
5936
|
+
cancelled = true;
|
|
5937
|
+
};
|
|
5938
|
+
}, [
|
|
5939
|
+
ready,
|
|
5940
|
+
authenticated,
|
|
5941
|
+
state.step,
|
|
5942
|
+
apiBaseUrl,
|
|
5943
|
+
getAccessToken,
|
|
5944
|
+
state.activeCredentialId,
|
|
5945
|
+
handleAuthorizedMobileReturn,
|
|
5946
|
+
onComplete,
|
|
5947
|
+
polling
|
|
5948
|
+
]);
|
|
5949
|
+
react.useEffect(() => {
|
|
5950
|
+
const loadAction = resolveDataLoadAction({
|
|
5951
|
+
authenticated,
|
|
5952
|
+
step: state.step,
|
|
5953
|
+
accountsCount: state.accounts.length,
|
|
5954
|
+
hasActiveCredential: !!state.activeCredentialId,
|
|
5955
|
+
loading: loadingDataRef.current
|
|
5956
|
+
});
|
|
5957
|
+
if (loadAction === "reset") {
|
|
5958
|
+
loadingDataRef.current = false;
|
|
5959
|
+
dispatch({ type: "DATA_LOAD_END" });
|
|
5960
|
+
return;
|
|
5840
5961
|
}
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
chainName: selectSourceChainName,
|
|
5890
|
-
tokenSymbol: selectSourceTokenSymbol
|
|
5962
|
+
if (loadAction === "wait") return;
|
|
5963
|
+
const credentialId = state.activeCredentialId;
|
|
5964
|
+
if (!credentialId) {
|
|
5965
|
+
loadingDataRef.current = false;
|
|
5966
|
+
dispatch({ type: "DATA_LOAD_END" });
|
|
5967
|
+
return;
|
|
5968
|
+
}
|
|
5969
|
+
let cancelled = false;
|
|
5970
|
+
loadingDataRef.current = true;
|
|
5971
|
+
const load = async () => {
|
|
5972
|
+
dispatch({ type: "DATA_LOAD_START" });
|
|
5973
|
+
try {
|
|
5974
|
+
const token = await getAccessToken();
|
|
5975
|
+
if (!token) throw new Error("Not authenticated");
|
|
5976
|
+
const [prov, accts, chn] = await Promise.all([
|
|
5977
|
+
fetchProviders(apiBaseUrl, token),
|
|
5978
|
+
fetchAccounts(apiBaseUrl, token, credentialId),
|
|
5979
|
+
fetchChains(apiBaseUrl, token)
|
|
5980
|
+
]);
|
|
5981
|
+
if (cancelled) return;
|
|
5982
|
+
const parsedAmt = depositAmount != null ? depositAmount : 0;
|
|
5983
|
+
const defaults = computeSmartDefaults(accts, parsedAmt);
|
|
5984
|
+
const persisted = loadMobileFlowState();
|
|
5985
|
+
const resolved = resolvePostAuthStep({
|
|
5986
|
+
hasPasskey: !!state.activeCredentialId,
|
|
5987
|
+
accounts: accts,
|
|
5988
|
+
persistedMobileFlow: persisted,
|
|
5989
|
+
mobileSetupInProgress: mobileSetupFlowRef.current,
|
|
5990
|
+
connectingNewAccount: state.connectingNewAccount
|
|
5991
|
+
});
|
|
5992
|
+
const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
|
|
5993
|
+
dispatch({
|
|
5994
|
+
type: "DATA_LOADED",
|
|
5995
|
+
providers: prov,
|
|
5996
|
+
accounts: accts,
|
|
5997
|
+
chains: chn,
|
|
5998
|
+
defaults,
|
|
5999
|
+
fallbackProviderId: !defaults && prov.length > 0 ? prov[0].id : null,
|
|
6000
|
+
resolvedStep: correctableSteps.includes(state.step) ? resolved.step : void 0,
|
|
6001
|
+
clearMobileState: resolved.clearPersistedFlow
|
|
6002
|
+
});
|
|
6003
|
+
if (resolved.clearPersistedFlow) clearMobileFlowState();
|
|
6004
|
+
} catch (err) {
|
|
6005
|
+
if (!cancelled) {
|
|
6006
|
+
captureException(err);
|
|
6007
|
+
dispatch({
|
|
6008
|
+
type: "SET_ERROR",
|
|
6009
|
+
error: err instanceof Error ? err.message : "Failed to load data"
|
|
5891
6010
|
});
|
|
5892
|
-
}
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
const succeeded = transfer?.status === "COMPLETED";
|
|
5899
|
-
const displayAmount = transfer?.amount?.amount ?? 0;
|
|
5900
|
-
const displayCurrency = transfer?.amount?.currency ?? "USD";
|
|
5901
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5902
|
-
SuccessScreen,
|
|
5903
|
-
{
|
|
5904
|
-
amount: displayAmount,
|
|
5905
|
-
currency: displayCurrency,
|
|
5906
|
-
succeeded,
|
|
5907
|
-
error,
|
|
5908
|
-
merchantName,
|
|
5909
|
-
sourceName,
|
|
5910
|
-
remainingLimit: succeeded ? (() => {
|
|
5911
|
-
const limit = selectedAccount?.remainingAllowance ?? oneTapLimit;
|
|
5912
|
-
return limit > displayAmount ? limit - displayAmount : 0;
|
|
5913
|
-
})() : void 0,
|
|
5914
|
-
onDone: onDismiss ?? handleNewPayment,
|
|
5915
|
-
onLogout: handleLogout,
|
|
5916
|
-
autoCloseSeconds
|
|
6011
|
+
}
|
|
6012
|
+
} finally {
|
|
6013
|
+
if (!cancelled) {
|
|
6014
|
+
loadingDataRef.current = false;
|
|
6015
|
+
dispatch({ type: "DATA_LOAD_END" });
|
|
6016
|
+
}
|
|
5917
6017
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
6018
|
+
};
|
|
6019
|
+
load();
|
|
6020
|
+
return () => {
|
|
6021
|
+
cancelled = true;
|
|
6022
|
+
loadingDataRef.current = false;
|
|
6023
|
+
};
|
|
6024
|
+
}, [
|
|
6025
|
+
authenticated,
|
|
6026
|
+
state.step,
|
|
6027
|
+
state.accounts.length,
|
|
6028
|
+
apiBaseUrl,
|
|
6029
|
+
getAccessToken,
|
|
6030
|
+
state.activeCredentialId,
|
|
6031
|
+
depositAmount,
|
|
6032
|
+
state.connectingNewAccount
|
|
6033
|
+
]);
|
|
6034
|
+
react.useEffect(() => {
|
|
6035
|
+
if (!polling.transfer) return;
|
|
6036
|
+
if (polling.transfer.status === "COMPLETED") {
|
|
6037
|
+
clearMobileFlowState();
|
|
6038
|
+
dispatch({ type: "TRANSFER_COMPLETED", transfer: polling.transfer });
|
|
6039
|
+
onComplete?.(polling.transfer);
|
|
6040
|
+
} else if (polling.transfer.status === "FAILED") {
|
|
6041
|
+
clearMobileFlowState();
|
|
6042
|
+
dispatch({ type: "TRANSFER_FAILED", transfer: polling.transfer, error: "Transfer failed." });
|
|
6043
|
+
}
|
|
6044
|
+
}, [polling.transfer, onComplete]);
|
|
6045
|
+
react.useEffect(() => {
|
|
6046
|
+
if (state.step !== "processing") {
|
|
6047
|
+
processingStartedAtRef.current = null;
|
|
6048
|
+
return;
|
|
6049
|
+
}
|
|
6050
|
+
if (!processingStartedAtRef.current) {
|
|
6051
|
+
processingStartedAtRef.current = Date.now();
|
|
6052
|
+
}
|
|
6053
|
+
const elapsedMs = Date.now() - processingStartedAtRef.current;
|
|
6054
|
+
const remainingMs = PROCESSING_TIMEOUT_MS - elapsedMs;
|
|
6055
|
+
const handleTimeout = () => {
|
|
6056
|
+
if (!hasProcessingTimedOut(processingStartedAtRef.current, Date.now())) return;
|
|
6057
|
+
const status = getTransferStatus(polling.transfer, state.transfer);
|
|
6058
|
+
const msg = buildProcessingTimeoutMessage(status);
|
|
6059
|
+
captureException(new Error(msg));
|
|
6060
|
+
polling.stopPolling();
|
|
6061
|
+
dispatch({ type: "PROCESSING_TIMEOUT", error: msg });
|
|
6062
|
+
onError?.(msg);
|
|
6063
|
+
};
|
|
6064
|
+
if (remainingMs <= 0) {
|
|
6065
|
+
handleTimeout();
|
|
6066
|
+
return;
|
|
6067
|
+
}
|
|
6068
|
+
const timeoutId = window.setTimeout(handleTimeout, remainingMs);
|
|
6069
|
+
return () => window.clearTimeout(timeoutId);
|
|
6070
|
+
}, [state.step, polling.transfer, state.transfer, polling.stopPolling, onError]);
|
|
6071
|
+
react.useEffect(() => {
|
|
6072
|
+
if (!state.mobileFlow) {
|
|
6073
|
+
handlingMobileReturnRef.current = false;
|
|
6074
|
+
return;
|
|
6075
|
+
}
|
|
6076
|
+
if (handlingMobileReturnRef.current) return;
|
|
6077
|
+
const polledTransfer = polling.transfer;
|
|
6078
|
+
if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
|
|
6079
|
+
void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
|
|
6080
|
+
}, [state.mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
|
|
6081
|
+
react.useEffect(() => {
|
|
6082
|
+
if (!state.mobileFlow) return;
|
|
6083
|
+
if (handlingMobileReturnRef.current) return;
|
|
6084
|
+
const transferIdToResume = pollingTransferIdRef.current ?? state.transfer?.id;
|
|
6085
|
+
if (!transferIdToResume) return;
|
|
6086
|
+
if (!polling.isPolling) polling.startPolling(transferIdToResume);
|
|
6087
|
+
const handleVisibility = () => {
|
|
6088
|
+
if (document.visibilityState === "visible" && !handlingMobileReturnRef.current) {
|
|
6089
|
+
polling.startPolling(transferIdToResume);
|
|
5939
6090
|
}
|
|
6091
|
+
};
|
|
6092
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
6093
|
+
return () => document.removeEventListener("visibilitychange", handleVisibility);
|
|
6094
|
+
}, [state.mobileFlow, state.transfer?.id, polling.isPolling, polling.startPolling]);
|
|
6095
|
+
react.useEffect(() => {
|
|
6096
|
+
if (!pendingSelectSourceAction) {
|
|
6097
|
+
initializedSelectSourceActionRef.current = null;
|
|
6098
|
+
setSelectSourceChainName("");
|
|
6099
|
+
setSelectSourceTokenSymbol("");
|
|
6100
|
+
return;
|
|
6101
|
+
}
|
|
6102
|
+
if (initializedSelectSourceActionRef.current === pendingSelectSourceAction.id) return;
|
|
6103
|
+
const hasRecommended = !!selectSourceRecommended && selectSourceChoices.some(
|
|
6104
|
+
(chain) => chain.chainName === selectSourceRecommended.chainName && chain.tokens.some((t) => t.tokenSymbol === selectSourceRecommended.tokenSymbol)
|
|
5940
6105
|
);
|
|
5941
|
-
|
|
5942
|
-
|
|
6106
|
+
if (hasRecommended && selectSourceRecommended) {
|
|
6107
|
+
setSelectSourceChainName(selectSourceRecommended.chainName);
|
|
6108
|
+
setSelectSourceTokenSymbol(selectSourceRecommended.tokenSymbol);
|
|
6109
|
+
} else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
|
|
6110
|
+
setSelectSourceChainName(selectSourceChoices[0].chainName);
|
|
6111
|
+
setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
|
|
6112
|
+
} else {
|
|
6113
|
+
setSelectSourceChainName("Base");
|
|
6114
|
+
setSelectSourceTokenSymbol("USDC");
|
|
6115
|
+
}
|
|
6116
|
+
initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
|
|
6117
|
+
}, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
|
|
6118
|
+
react.useEffect(() => {
|
|
6119
|
+
if (pendingSelectSourceAction && state.step === "processing") {
|
|
6120
|
+
preSelectSourceStepRef.current = state.step;
|
|
6121
|
+
dispatch({ type: "NAVIGATE", step: "select-source" });
|
|
6122
|
+
} else if (!pendingSelectSourceAction && state.step === "select-source") {
|
|
6123
|
+
dispatch({ type: "NAVIGATE", step: preSelectSourceStepRef.current ?? "processing" });
|
|
6124
|
+
preSelectSourceStepRef.current = null;
|
|
6125
|
+
}
|
|
6126
|
+
}, [pendingSelectSourceAction, state.step]);
|
|
6127
|
+
const handlers = react.useMemo(() => ({
|
|
6128
|
+
onSendLoginCode: handleSendLoginCode,
|
|
6129
|
+
onVerifyLoginCode: handleVerifyLoginCode,
|
|
6130
|
+
onResendLoginCode: handleResendLoginCode,
|
|
6131
|
+
onBackFromOtp: () => {
|
|
6132
|
+
setOtpCode("");
|
|
6133
|
+
dispatch({ type: "BACK_TO_LOGIN" });
|
|
6134
|
+
},
|
|
6135
|
+
onRegisterPasskey: handleRegisterPasskey,
|
|
6136
|
+
onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup,
|
|
6137
|
+
onVerifyPasskeyViaPopup: handleVerifyPasskeyViaPopup,
|
|
6138
|
+
onSelectProvider: handleSelectProvider,
|
|
6139
|
+
onContinueConnection: handleContinueConnection,
|
|
6140
|
+
onPay: handlePay,
|
|
6141
|
+
onIncreaseLimit: handleIncreaseLimit,
|
|
6142
|
+
onConfirmSign: handleConfirmSign,
|
|
6143
|
+
onRetryMobileStatus: handleRetryMobileStatus,
|
|
6144
|
+
onLogout: handleLogout,
|
|
6145
|
+
onNewPayment: handleNewPayment,
|
|
6146
|
+
onNavigate: (step) => dispatch({ type: "NAVIGATE", step }),
|
|
6147
|
+
onSetAuthInput: setAuthInput,
|
|
6148
|
+
onSetOtpCode: (code) => {
|
|
6149
|
+
setOtpCode(code);
|
|
6150
|
+
dispatch({ type: "SET_ERROR", error: null });
|
|
6151
|
+
},
|
|
6152
|
+
onSelectSourceChainChange: handleSelectSourceChainChange,
|
|
6153
|
+
onSetSelectSourceTokenSymbol: setSelectSourceTokenSymbol,
|
|
6154
|
+
onConfirmSelectSource: handleConfirmSelectSource
|
|
6155
|
+
}), [
|
|
6156
|
+
handleSendLoginCode,
|
|
6157
|
+
handleVerifyLoginCode,
|
|
6158
|
+
handleResendLoginCode,
|
|
6159
|
+
handleRegisterPasskey,
|
|
6160
|
+
handleCreatePasskeyViaPopup,
|
|
6161
|
+
handleVerifyPasskeyViaPopup,
|
|
6162
|
+
handleSelectProvider,
|
|
6163
|
+
handleContinueConnection,
|
|
6164
|
+
handlePay,
|
|
6165
|
+
handleIncreaseLimit,
|
|
6166
|
+
handleConfirmSign,
|
|
6167
|
+
handleRetryMobileStatus,
|
|
6168
|
+
handleLogout,
|
|
6169
|
+
handleNewPayment,
|
|
6170
|
+
handleSelectSourceChainChange,
|
|
6171
|
+
handleConfirmSelectSource
|
|
6172
|
+
]);
|
|
6173
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6174
|
+
StepRenderer,
|
|
6175
|
+
{
|
|
6176
|
+
state,
|
|
6177
|
+
ready,
|
|
6178
|
+
authenticated,
|
|
6179
|
+
activeOtpStatus,
|
|
6180
|
+
pollingTransfer: polling.transfer,
|
|
6181
|
+
pollingError: polling.error,
|
|
6182
|
+
authExecutorError: authExecutor.error,
|
|
6183
|
+
transferSigningSigning: transferSigning.signing,
|
|
6184
|
+
transferSigningError: transferSigning.error,
|
|
6185
|
+
pendingConnections,
|
|
6186
|
+
sourceName,
|
|
6187
|
+
sourceAddress,
|
|
6188
|
+
sourceVerified,
|
|
6189
|
+
maxSourceBalance,
|
|
6190
|
+
tokenCount,
|
|
6191
|
+
selectedAccount,
|
|
6192
|
+
selectSourceChoices,
|
|
6193
|
+
selectSourceRecommended,
|
|
6194
|
+
authInput,
|
|
6195
|
+
otpCode,
|
|
6196
|
+
selectSourceChainName,
|
|
6197
|
+
selectSourceTokenSymbol,
|
|
6198
|
+
merchantName,
|
|
6199
|
+
onBack,
|
|
6200
|
+
onDismiss,
|
|
6201
|
+
autoCloseSeconds,
|
|
6202
|
+
depositAmount,
|
|
6203
|
+
handlers
|
|
6204
|
+
}
|
|
6205
|
+
);
|
|
5943
6206
|
}
|
|
5944
6207
|
|
|
5945
6208
|
exports.AdvancedSourceScreen = AdvancedSourceScreen;
|