@b3dotfun/sdk 0.1.70-alpha.15 → 0.1.70-alpha.17
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/cjs/anyspend/react/components/AnySpend.d.ts +2 -1
- package/dist/cjs/anyspend/react/components/AnySpend.js +81 -16
- package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +4 -1
- package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +16 -2
- package/dist/cjs/anyspend/react/components/QRDeposit.d.ts +9 -1
- package/dist/cjs/anyspend/react/components/QRDeposit.js +3 -3
- package/dist/cjs/anyspend/react/components/common/PureTransferView.d.ts +30 -0
- package/dist/cjs/anyspend/react/components/common/PureTransferView.js +64 -0
- package/dist/cjs/global-account/client-manager.js +32 -0
- package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -1
- package/dist/esm/anyspend/react/components/AnySpend.js +81 -16
- package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +4 -1
- package/dist/esm/anyspend/react/components/AnySpendDeposit.js +16 -2
- package/dist/esm/anyspend/react/components/QRDeposit.d.ts +9 -1
- package/dist/esm/anyspend/react/components/QRDeposit.js +4 -4
- package/dist/esm/anyspend/react/components/common/PureTransferView.d.ts +30 -0
- package/dist/esm/anyspend/react/components/common/PureTransferView.js +61 -0
- package/dist/esm/global-account/client-manager.js +32 -0
- package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -1
- package/dist/types/anyspend/react/components/QRDeposit.d.ts +9 -1
- package/dist/types/anyspend/react/components/common/PureTransferView.d.ts +30 -0
- package/package.json +1 -1
- package/src/anyspend/react/components/AnySpend.tsx +108 -18
- package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +5 -1
- package/src/anyspend/react/components/AnySpendDeposit.tsx +18 -1
- package/src/anyspend/react/components/QRDeposit.tsx +55 -4
- package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +39 -1
- package/src/anyspend/react/components/common/PureTransferView.tsx +212 -0
- package/src/global-account/client-manager.ts +33 -0
|
@@ -71,6 +71,7 @@ import { LoginStep } from "@b3dotfun/sdk/global-account/react/components/SignInW
|
|
|
71
71
|
import { PanelOnramp } from "./common/PanelOnramp";
|
|
72
72
|
import { PanelOnrampPayment } from "./common/PanelOnrampPayment";
|
|
73
73
|
import { PointsDetailPanel } from "./common/PointsDetailPanel";
|
|
74
|
+
import { PureTransferView } from "./common/PureTransferView";
|
|
74
75
|
import { RecipientSelection } from "./common/RecipientSelection";
|
|
75
76
|
import { TabSection } from "./common/TabSection";
|
|
76
77
|
import { AnySpendCustomizationProvider, useAnySpendCustomization } from "./context/AnySpendCustomizationContext";
|
|
@@ -100,6 +101,7 @@ export enum PanelView {
|
|
|
100
101
|
DIRECT_TRANSFER_SUCCESS,
|
|
101
102
|
FIAT_KYC,
|
|
102
103
|
FIAT_AUTH,
|
|
104
|
+
PURE_TRANSFER_ADDRESS,
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
const ANYSPEND_RECIPIENTS_KEY = "anyspend_recipients";
|
|
@@ -687,7 +689,10 @@ function AnySpendInner({
|
|
|
687
689
|
globalAddress,
|
|
688
690
|
});
|
|
689
691
|
|
|
690
|
-
const recipientProfile = useProfile({
|
|
692
|
+
const recipientProfile = useProfile({
|
|
693
|
+
address: effectiveRecipientAddress,
|
|
694
|
+
fresh: true,
|
|
695
|
+
});
|
|
691
696
|
const recipientName = recipientProfile.data?.name;
|
|
692
697
|
|
|
693
698
|
// Check token balance for crypto payments
|
|
@@ -906,15 +911,36 @@ function AnySpendInner({
|
|
|
906
911
|
const isDirectTransfer = isSameChainSameToken && allowDirectTransfer;
|
|
907
912
|
|
|
908
913
|
// Determine button state and text
|
|
909
|
-
const btnInfo: {
|
|
914
|
+
const btnInfo: {
|
|
915
|
+
text: string;
|
|
916
|
+
disable: boolean;
|
|
917
|
+
error: boolean;
|
|
918
|
+
loading: boolean;
|
|
919
|
+
} = useMemo(() => {
|
|
910
920
|
// For fiat tab, check srcAmountOnRamp; for crypto tab, check activeInputAmountInWei
|
|
911
921
|
const hasAmount =
|
|
912
922
|
activeTab === "fiat" ? srcAmountOnRamp && parseFloat(srcAmountOnRamp) > 0 : activeInputAmountInWei !== "0";
|
|
913
|
-
if (!hasAmount)
|
|
923
|
+
if (!hasAmount)
|
|
924
|
+
return {
|
|
925
|
+
text: "Enter an amount",
|
|
926
|
+
disable: true,
|
|
927
|
+
error: false,
|
|
928
|
+
loading: false,
|
|
929
|
+
};
|
|
914
930
|
if (isSameChainSameToken && !allowDirectTransfer)
|
|
915
|
-
return {
|
|
931
|
+
return {
|
|
932
|
+
text: "Select a different token or chain",
|
|
933
|
+
disable: true,
|
|
934
|
+
error: false,
|
|
935
|
+
loading: false,
|
|
936
|
+
};
|
|
916
937
|
if (isLoadingAnyspendQuote && !isSameChainSameToken)
|
|
917
|
-
return {
|
|
938
|
+
return {
|
|
939
|
+
text: "Loading quote...",
|
|
940
|
+
disable: true,
|
|
941
|
+
error: false,
|
|
942
|
+
loading: true,
|
|
943
|
+
};
|
|
918
944
|
if (isCreatingOrder || isCreatingOnrampOrder || isSwitchingOrExecuting)
|
|
919
945
|
return {
|
|
920
946
|
text: isSwitchingOrExecuting ? "Transferring..." : "Creating order...",
|
|
@@ -923,15 +949,31 @@ function AnySpendInner({
|
|
|
923
949
|
loading: true,
|
|
924
950
|
};
|
|
925
951
|
if ((!anyspendQuote || !anyspendQuote.success) && !(isSameChainSameToken && allowDirectTransfer))
|
|
926
|
-
return {
|
|
952
|
+
return {
|
|
953
|
+
text: "No quote found",
|
|
954
|
+
disable: true,
|
|
955
|
+
error: false,
|
|
956
|
+
loading: false,
|
|
957
|
+
};
|
|
927
958
|
|
|
928
959
|
if (activeTab === "fiat") {
|
|
929
960
|
// For fiat: check recipient first, then payment method
|
|
930
|
-
if (!effectiveRecipientAddress)
|
|
961
|
+
if (!effectiveRecipientAddress)
|
|
962
|
+
return {
|
|
963
|
+
text: "Select recipient",
|
|
964
|
+
disable: false,
|
|
965
|
+
error: false,
|
|
966
|
+
loading: false,
|
|
967
|
+
};
|
|
931
968
|
|
|
932
969
|
// If no fiat payment method selected, show "Select payment method"
|
|
933
970
|
if (selectedFiatPaymentMethod === FiatPaymentMethod.NONE) {
|
|
934
|
-
return {
|
|
971
|
+
return {
|
|
972
|
+
text: "Select payment method",
|
|
973
|
+
disable: false,
|
|
974
|
+
error: false,
|
|
975
|
+
loading: false,
|
|
976
|
+
};
|
|
935
977
|
}
|
|
936
978
|
// If payment method is selected, show "Continue"
|
|
937
979
|
return { text: "Continue", disable: false, error: false, loading: false };
|
|
@@ -942,11 +984,22 @@ function AnySpendInner({
|
|
|
942
984
|
|
|
943
985
|
// If no payment method selected, show "Choose payment method"
|
|
944
986
|
if (effectiveCryptoPaymentMethod === CryptoPaymentMethodType.NONE) {
|
|
945
|
-
return {
|
|
987
|
+
return {
|
|
988
|
+
text: "Choose payment method",
|
|
989
|
+
disable: false,
|
|
990
|
+
error: false,
|
|
991
|
+
loading: false,
|
|
992
|
+
};
|
|
946
993
|
}
|
|
947
994
|
|
|
948
995
|
// Check recipient after payment method
|
|
949
|
-
if (!effectiveRecipientAddress)
|
|
996
|
+
if (!effectiveRecipientAddress)
|
|
997
|
+
return {
|
|
998
|
+
text: "Select recipient",
|
|
999
|
+
disable: false,
|
|
1000
|
+
error: false,
|
|
1001
|
+
loading: false,
|
|
1002
|
+
};
|
|
950
1003
|
|
|
951
1004
|
// If payment method selected, show appropriate action
|
|
952
1005
|
if (
|
|
@@ -954,10 +1007,22 @@ function AnySpendInner({
|
|
|
954
1007
|
effectiveCryptoPaymentMethod === CryptoPaymentMethodType.GLOBAL_WALLET
|
|
955
1008
|
) {
|
|
956
1009
|
const buttonText = isSameChainSameToken && allowDirectTransfer ? "Transfer" : "Swap";
|
|
957
|
-
return {
|
|
1010
|
+
return {
|
|
1011
|
+
text: buttonText,
|
|
1012
|
+
disable: false,
|
|
1013
|
+
error: false,
|
|
1014
|
+
loading: false,
|
|
1015
|
+
};
|
|
958
1016
|
}
|
|
959
1017
|
if (effectiveCryptoPaymentMethod === CryptoPaymentMethodType.TRANSFER_CRYPTO) {
|
|
960
|
-
|
|
1018
|
+
const transferText =
|
|
1019
|
+
isSameChainSameToken && allowDirectTransfer ? "Show deposit address" : "Continue to payment";
|
|
1020
|
+
return {
|
|
1021
|
+
text: transferText,
|
|
1022
|
+
disable: false,
|
|
1023
|
+
error: false,
|
|
1024
|
+
loading: false,
|
|
1025
|
+
};
|
|
961
1026
|
}
|
|
962
1027
|
}
|
|
963
1028
|
|
|
@@ -1063,10 +1128,18 @@ function AnySpendInner({
|
|
|
1063
1128
|
// Handle crypto swap creation
|
|
1064
1129
|
const handleCryptoSwap = async (method: CryptoPaymentMethodType) => {
|
|
1065
1130
|
try {
|
|
1066
|
-
|
|
1131
|
+
// TRANSFER_CRYPTO is the manual deposit-address flow — no connected wallet to send from.
|
|
1132
|
+
const isDirectTransfer =
|
|
1133
|
+
isSameChainSameToken && allowDirectTransfer && method !== CryptoPaymentMethodType.TRANSFER_CRYPTO;
|
|
1067
1134
|
|
|
1068
1135
|
invariant(effectiveRecipientAddress, "Recipient address is not found");
|
|
1069
1136
|
|
|
1137
|
+
// Same-token manual deposit → show recipient address + watch, no relayer order, no fee
|
|
1138
|
+
if (isSameChainSameToken && allowDirectTransfer && method === CryptoPaymentMethodType.TRANSFER_CRYPTO) {
|
|
1139
|
+
navigateToPanel(PanelView.PURE_TRANSFER_ADDRESS, "forward");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1070
1143
|
const srcAmountBigInt = parseUnits(srcAmount.replace(/,/g, ""), selectedSrcToken.decimals);
|
|
1071
1144
|
|
|
1072
1145
|
// Handle direct transfer (same chain/token) - bypass backend, transfer directly
|
|
@@ -1089,10 +1162,6 @@ function AnySpendInner({
|
|
|
1089
1162
|
// Regular swap flow - use backend
|
|
1090
1163
|
invariant(anyspendQuote, "Relay price is not found");
|
|
1091
1164
|
|
|
1092
|
-
// Debug: Check payment method values
|
|
1093
|
-
console.log("handleCryptoSwap - method parameter:", method);
|
|
1094
|
-
console.log("handleCryptoSwap - selectedCryptoPaymentMethod state:", selectedCryptoPaymentMethod);
|
|
1095
|
-
|
|
1096
1165
|
createOrder({
|
|
1097
1166
|
recipientAddress: effectiveRecipientAddress,
|
|
1098
1167
|
orderType: "swap",
|
|
@@ -1100,7 +1169,11 @@ function AnySpendInner({
|
|
|
1100
1169
|
dstChain: isBuyMode ? destinationTokenChainId : selectedDstChainId,
|
|
1101
1170
|
srcToken: selectedSrcToken,
|
|
1102
1171
|
dstToken: isBuyMode
|
|
1103
|
-
? {
|
|
1172
|
+
? {
|
|
1173
|
+
...selectedDstToken,
|
|
1174
|
+
chainId: destinationTokenChainId,
|
|
1175
|
+
address: destinationTokenAddress,
|
|
1176
|
+
}
|
|
1104
1177
|
: selectedDstToken,
|
|
1105
1178
|
srcAmount: srcAmountBigInt.toString(),
|
|
1106
1179
|
expectedDstAmount: anyspendQuote?.data?.currencyOut?.amount || "0",
|
|
@@ -1759,6 +1832,20 @@ function AnySpendInner({
|
|
|
1759
1832
|
</div>
|
|
1760
1833
|
);
|
|
1761
1834
|
|
|
1835
|
+
const pureTransferAddressView = (
|
|
1836
|
+
<PureTransferView
|
|
1837
|
+
mode={mode}
|
|
1838
|
+
recipientAddress={effectiveRecipientAddress ?? ""}
|
|
1839
|
+
chainId={selectedSrcChainId}
|
|
1840
|
+
token={selectedSrcToken}
|
|
1841
|
+
displayAmount={srcAmount && srcAmount !== "0" ? `${srcAmount} ${selectedSrcToken.symbol}` : undefined}
|
|
1842
|
+
isActive={activePanel === PanelView.PURE_TRANSFER_ADDRESS}
|
|
1843
|
+
onBack={navigateBack}
|
|
1844
|
+
onClose={() => setB3ModalOpen(false)}
|
|
1845
|
+
onTransferDetected={() => onSuccess?.()}
|
|
1846
|
+
/>
|
|
1847
|
+
);
|
|
1848
|
+
|
|
1762
1849
|
// Add tabs to the main component when no order is loaded
|
|
1763
1850
|
return (
|
|
1764
1851
|
<StyleRoot>
|
|
@@ -1839,6 +1926,9 @@ function AnySpendInner({
|
|
|
1839
1926
|
<div key="fiat-auth-view" className={cn(mode === "page" && "p-6")}>
|
|
1840
1927
|
{authView}
|
|
1841
1928
|
</div>,
|
|
1929
|
+
<div key="pure-transfer-address-view" className={cn(mode === "page" && "p-6")}>
|
|
1930
|
+
{pureTransferAddressView}
|
|
1931
|
+
</div>,
|
|
1842
1932
|
]}
|
|
1843
1933
|
</TransitionPanel>
|
|
1844
1934
|
</div>
|
|
@@ -574,7 +574,11 @@ function AnySpendCustomExactInInner({
|
|
|
574
574
|
|
|
575
575
|
const handleCryptoOrder = async () => {
|
|
576
576
|
try {
|
|
577
|
-
|
|
577
|
+
// TRANSFER_CRYPTO is the manual deposit-address flow — no connected wallet to send from.
|
|
578
|
+
const isDirectTransfer =
|
|
579
|
+
isSameChainSameToken &&
|
|
580
|
+
allowDirectTransfer &&
|
|
581
|
+
effectiveCryptoPaymentMethod !== CryptoPaymentMethodType.TRANSFER_CRYPTO;
|
|
578
582
|
|
|
579
583
|
if (!isDirectTransfer) {
|
|
580
584
|
invariant(anyspendQuote, "Relay price is not found");
|
|
@@ -3,6 +3,7 @@ import { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
|
3
3
|
import { GetQuoteResponse } from "@b3dotfun/sdk/anyspend/types/api_req_res";
|
|
4
4
|
import { Skeleton, useAccountWallet, useSimBalance, useTokenData } from "@b3dotfun/sdk/global-account/react";
|
|
5
5
|
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
6
|
+
import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
|
|
6
7
|
import {
|
|
7
8
|
NetworkArbitrumOne,
|
|
8
9
|
NetworkBase,
|
|
@@ -631,7 +632,9 @@ export function AnySpendDeposit({
|
|
|
631
632
|
"anyspend-deposit-option-description text-as-secondary text-xs"
|
|
632
633
|
}
|
|
633
634
|
>
|
|
634
|
-
|
|
635
|
+
{pureTransferOnly
|
|
636
|
+
? "Send tokens directly to deposit address"
|
|
637
|
+
: "Send any token — we'll convert it for you"}
|
|
635
638
|
</p>
|
|
636
639
|
</div>
|
|
637
640
|
</div>
|
|
@@ -700,6 +703,19 @@ export function AnySpendDeposit({
|
|
|
700
703
|
// `KNOWN_FUNDING_TOKENS` key — so `hasKnownDestination` is always true and this
|
|
701
704
|
// branch never fires for them. The guard is defensive for FUTURE callers that pass
|
|
702
705
|
// an unknown token together with `pureTransferOnly=true`.
|
|
706
|
+
// Only surface the fixed destination amount on the QR path when its decimals are trustworthy
|
|
707
|
+
// (a known funding token or a resolved API lookup) — otherwise `destinationToken.decimals`
|
|
708
|
+
// is the generic 18 fallback and formatting a 6-decimal token's wei against it would be off by
|
|
709
|
+
// 10^12. When unresolved we still show the destination token/chain, just without an amount.
|
|
710
|
+
const destinationDecimalsResolved = hasKnownDestination || !!destinationTokenData;
|
|
711
|
+
const destinationAmountDisplay =
|
|
712
|
+
destinationTokenAmount &&
|
|
713
|
+
/^\d+$/.test(destinationTokenAmount) &&
|
|
714
|
+
BigInt(destinationTokenAmount) > 0n &&
|
|
715
|
+
destinationDecimalsResolved
|
|
716
|
+
? formatTokenAmount(BigInt(destinationTokenAmount), destinationToken.decimals, 6, false)
|
|
717
|
+
: undefined;
|
|
718
|
+
|
|
703
719
|
if (pureTransferOnly && !hasKnownDestination && !destinationTokenData && isDestinationTokenLoading) {
|
|
704
720
|
return (
|
|
705
721
|
<div
|
|
@@ -722,6 +738,7 @@ export function AnySpendDeposit({
|
|
|
722
738
|
recipientAddress={recipientAddress}
|
|
723
739
|
destinationToken={destinationToken}
|
|
724
740
|
destinationChainId={destinationTokenChainId}
|
|
741
|
+
destinationAmountDisplay={destinationAmountDisplay}
|
|
725
742
|
pureTransferOnly={pureTransferOnly}
|
|
726
743
|
depositContractConfig={depositContractConfig}
|
|
727
744
|
onBack={handleBack}
|
|
@@ -36,6 +36,14 @@ export interface QRDepositProps {
|
|
|
36
36
|
destinationToken: components["schemas"]["Token"];
|
|
37
37
|
/** The destination chain ID */
|
|
38
38
|
destinationChainId: number;
|
|
39
|
+
/**
|
|
40
|
+
* Human-formatted destination amount (e.g. "9.365547"), shown as an APPROXIMATE
|
|
41
|
+
* target in swap mode only. The QR deposit path accepts an arbitrary sent amount,
|
|
42
|
+
* so this is never a guaranteed receive figure — the UI frames it as "any amount
|
|
43
|
+
* works". Omitted when no fixed amount is set or the destination decimals are
|
|
44
|
+
* unresolved (to avoid a mis-scaled number). Ignored in pure-transfer mode.
|
|
45
|
+
*/
|
|
46
|
+
destinationAmountDisplay?: string;
|
|
39
47
|
/**
|
|
40
48
|
* When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
|
|
41
49
|
* the user's currently selected source token/chain, so the exact token the user
|
|
@@ -96,6 +104,7 @@ export function QRDeposit({
|
|
|
96
104
|
sourceChainId: sourceChainIdProp,
|
|
97
105
|
destinationToken,
|
|
98
106
|
destinationChainId,
|
|
107
|
+
destinationAmountDisplay,
|
|
99
108
|
pureTransferOnly = false,
|
|
100
109
|
creatorAddress,
|
|
101
110
|
depositContractConfig,
|
|
@@ -410,6 +419,32 @@ export function QRDeposit({
|
|
|
410
419
|
/>
|
|
411
420
|
</div>
|
|
412
421
|
|
|
422
|
+
{/* Receive (swap mode only) — makes the conversion explicit: what the sent token
|
|
423
|
+
becomes and on which chain. Gated on the computed isPureTransfer (not the static
|
|
424
|
+
prop) so a runtime source change back to the same token correctly hides it. */}
|
|
425
|
+
{!isPureTransfer && (
|
|
426
|
+
<div className="anyspend-qr-receive flex flex-col gap-1.5">
|
|
427
|
+
<label className="anyspend-qr-receive-label text-as-secondary text-sm">Receive</label>
|
|
428
|
+
<div className="anyspend-qr-receive-row border-as-stroke bg-as-surface-secondary flex w-full items-center gap-2 rounded-xl border px-3 py-2.5">
|
|
429
|
+
{effectiveDestinationToken.metadata?.logoURI ? (
|
|
430
|
+
<ChainTokenIcon
|
|
431
|
+
chainUrl={ALL_CHAINS[effectiveDestinationChainId]?.logoUrl}
|
|
432
|
+
tokenUrl={effectiveDestinationToken.metadata.logoURI}
|
|
433
|
+
className="h-8 min-h-8 w-8 min-w-8"
|
|
434
|
+
/>
|
|
435
|
+
) : (
|
|
436
|
+
<div className="h-8 w-8 rounded-full bg-gray-700" />
|
|
437
|
+
)}
|
|
438
|
+
<div className="flex flex-col items-start gap-0">
|
|
439
|
+
<div className="text-as-primary font-semibold">{effectiveDestinationToken.symbol || "Token"}</div>
|
|
440
|
+
<div className="text-as-primary/70 text-xs">
|
|
441
|
+
{ALL_CHAINS[effectiveDestinationChainId]?.name ?? "Unknown"}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
|
|
413
448
|
{/* QR Code and Address - horizontal layout */}
|
|
414
449
|
<div
|
|
415
450
|
className={
|
|
@@ -454,10 +489,26 @@ export function QRDeposit({
|
|
|
454
489
|
|
|
455
490
|
{/* Warnings */}
|
|
456
491
|
<ChainWarningText chainId={effectiveDestinationChainId} />
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
492
|
+
{isPureTransfer ? (
|
|
493
|
+
<WarningText>
|
|
494
|
+
Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"}. Other tokens
|
|
495
|
+
will not be converted.
|
|
496
|
+
</WarningText>
|
|
497
|
+
) : (
|
|
498
|
+
<>
|
|
499
|
+
<WarningText>
|
|
500
|
+
Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"} to this
|
|
501
|
+
address — it will be converted to {effectiveDestinationToken.symbol || "the destination token"} on{" "}
|
|
502
|
+
{ALL_CHAINS[effectiveDestinationChainId]?.name ?? "the destination chain"}.
|
|
503
|
+
</WarningText>
|
|
504
|
+
{destinationAmountDisplay && (
|
|
505
|
+
<p className="anyspend-qr-amount-hint text-as-secondary text-xs">
|
|
506
|
+
Target ≈ {destinationAmountDisplay} {effectiveDestinationToken.symbol || "tokens"}. Any amount of{" "}
|
|
507
|
+
{sourceToken.symbol || "tokens"} works — you'll receive whatever your deposit converts to.
|
|
508
|
+
</p>
|
|
509
|
+
)}
|
|
510
|
+
</>
|
|
511
|
+
)}
|
|
461
512
|
|
|
462
513
|
{/* Watching indicator for pure transfers */}
|
|
463
514
|
{isPureTransfer && isWatchingTransfer && (
|
|
@@ -164,6 +164,11 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
|
|
|
164
164
|
|
|
165
165
|
// The displayed deposit address is the recipient's own address, NOT a global/order address.
|
|
166
166
|
expect(screen.getByText(RECIPIENT)).toBeTruthy();
|
|
167
|
+
|
|
168
|
+
// Pure-transfer UI is unchanged: NO "Receive" row (there is no distinct destination),
|
|
169
|
+
// and the accurate "will not be converted" warning is kept verbatim.
|
|
170
|
+
expect(screen.queryByText("Receive")).toBeNull();
|
|
171
|
+
expect(screen.getByText(/Other tokens will not be converted/)).toBeTruthy();
|
|
167
172
|
});
|
|
168
173
|
|
|
169
174
|
it("Test B — switching the source token stays a pure transfer (no order; mirror follows selection)", async () => {
|
|
@@ -206,7 +211,14 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
|
|
|
206
211
|
|
|
207
212
|
// No pureTransferOnly: source defaults to ETH-on-Base, which differs from the USDC dest,
|
|
208
213
|
// so this is a real swap and the normal deposit_first order path must run.
|
|
209
|
-
|
|
214
|
+
const { container } = render(
|
|
215
|
+
<QRDeposit
|
|
216
|
+
recipientAddress={RECIPIENT}
|
|
217
|
+
destinationToken={USDC_BASE}
|
|
218
|
+
destinationChainId={8453}
|
|
219
|
+
destinationAmountDisplay="9.365547"
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
210
222
|
|
|
211
223
|
// The flag is what gates the pure-transfer behavior: without it, an order IS created once.
|
|
212
224
|
expect(createOrderSpy).toHaveBeenCalledTimes(1);
|
|
@@ -218,6 +230,32 @@ describe("QRDeposit pureTransferOnly (Fund Wallet)", () => {
|
|
|
218
230
|
expect(orderArgs.dstToken.decimals).toBe(6);
|
|
219
231
|
// Source defaulted to native ETH on Base (the differing token that forces a swap).
|
|
220
232
|
expect(orderArgs.srcToken.address).toBe("0x0000000000000000000000000000000000000000");
|
|
233
|
+
|
|
234
|
+
// Swap-mode UX clarity: a read-only "Receive" row now discloses the destination token…
|
|
235
|
+
expect(screen.getByText("Receive")).toBeTruthy();
|
|
236
|
+
// …the misleading "will not be converted" copy is gone…
|
|
237
|
+
expect(screen.queryByText(/will not be converted/)).toBeNull();
|
|
238
|
+
// …the warning states the conversion explicitly, and the fixed amount shows as an
|
|
239
|
+
// approximate target (never a guaranteed receive figure).
|
|
240
|
+
expect(container.textContent).toContain("it will be converted to USDC on Base");
|
|
241
|
+
expect(container.textContent).toContain("Target ≈ 9.365547 USDC");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("Test C2 — swap mode without a fixed amount shows the Receive row but no target hint", async () => {
|
|
245
|
+
const QRDeposit = await loadQRDeposit();
|
|
246
|
+
|
|
247
|
+
// Swap mode (source ETH-on-Base != USDC dest) but NO destinationAmountDisplay — the common
|
|
248
|
+
// open-ended funding case. The destination must still be disclosed, but there must be no
|
|
249
|
+
// dangling "Target ≈" line (and no blank amount).
|
|
250
|
+
const { container } = render(
|
|
251
|
+
<QRDeposit recipientAddress={RECIPIENT} destinationToken={USDC_BASE} destinationChainId={8453} />,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Destination is still disclosed…
|
|
255
|
+
expect(screen.getByText("Receive")).toBeTruthy();
|
|
256
|
+
expect(container.textContent).toContain("it will be converted to USDC on Base");
|
|
257
|
+
// …but no amount hint is rendered when no amount is provided.
|
|
258
|
+
expect(container.textContent).not.toContain("Target ≈");
|
|
221
259
|
});
|
|
222
260
|
});
|
|
223
261
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getChainName, getPaymentUrl, ZERO_ADDRESS } from "@b3dotfun/sdk/anyspend";
|
|
4
|
+
import type { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
5
|
+
import { CopyToClipboard } from "@b3dotfun/sdk/global-account/react";
|
|
6
|
+
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
7
|
+
import { ChevronLeft, Loader2, X } from "lucide-react";
|
|
8
|
+
import { QRCodeSVG } from "qrcode.react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
10
|
+
import type { TransferResult } from "../../hooks/useWatchTransfer";
|
|
11
|
+
import { useWatchTransfer } from "../../hooks/useWatchTransfer";
|
|
12
|
+
import { ChainWarningText, WarningText } from "./WarningText";
|
|
13
|
+
import { TransferResultScreen } from "./TransferResultScreen";
|
|
14
|
+
|
|
15
|
+
export interface PureTransferViewProps {
|
|
16
|
+
mode?: "modal" | "page";
|
|
17
|
+
/** The recipient address where funds will land (the user's own wallet) */
|
|
18
|
+
recipientAddress: string;
|
|
19
|
+
/** Chain ID to watch on */
|
|
20
|
+
chainId: number;
|
|
21
|
+
/** Token the user sends — drives the QR code and balance watcher */
|
|
22
|
+
token: components["schemas"]["Token"];
|
|
23
|
+
/** Human-readable amount to display (display-only; skip row if empty or "0") */
|
|
24
|
+
displayAmount?: string;
|
|
25
|
+
/** Whether this panel is the currently active panel; gates the balance watcher */
|
|
26
|
+
isActive?: boolean;
|
|
27
|
+
/** Callback when back chevron is clicked */
|
|
28
|
+
onBack?: () => void;
|
|
29
|
+
/** Callback when X close button is clicked */
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
/** Callback when an incoming transfer is detected */
|
|
32
|
+
onTransferDetected?: (result: TransferResult) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A self-contained view that shows the recipient address as a QR code + copy button and
|
|
37
|
+
* watches for an incoming same-chain/same-token transfer via `useWatchTransfer`. When a
|
|
38
|
+
* transfer is detected it renders `TransferResultScreen`.
|
|
39
|
+
*
|
|
40
|
+
* This is the "TRANSFER_CRYPTO" path for same-token AnySpend deposits — no relayer order,
|
|
41
|
+
* no fee; the user simply sends the token directly to their own address.
|
|
42
|
+
*/
|
|
43
|
+
export function PureTransferView({
|
|
44
|
+
mode = "modal",
|
|
45
|
+
recipientAddress,
|
|
46
|
+
chainId,
|
|
47
|
+
token,
|
|
48
|
+
displayAmount,
|
|
49
|
+
isActive,
|
|
50
|
+
onBack,
|
|
51
|
+
onClose,
|
|
52
|
+
onTransferDetected,
|
|
53
|
+
}: PureTransferViewProps) {
|
|
54
|
+
const [transferResult, setTransferResult] = useState<TransferResult | null>(null);
|
|
55
|
+
|
|
56
|
+
// Fix 4: reset transferResult when the panel becomes active again
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (isActive) setTransferResult(null);
|
|
59
|
+
}, [isActive]);
|
|
60
|
+
|
|
61
|
+
// Fix 3: stabilize the callback so it doesn't re-run the watcher effect every render
|
|
62
|
+
const handleTransferDetected = useCallback(
|
|
63
|
+
(result: TransferResult) => {
|
|
64
|
+
setTransferResult(result);
|
|
65
|
+
onTransferDetected?.(result);
|
|
66
|
+
},
|
|
67
|
+
[onTransferDetected],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { isWatching } = useWatchTransfer({
|
|
71
|
+
address: recipientAddress,
|
|
72
|
+
chainId,
|
|
73
|
+
tokenAddress: token.address,
|
|
74
|
+
tokenDecimals: token.decimals,
|
|
75
|
+
// Fix 1: also require a non-empty recipient before enabling the watcher
|
|
76
|
+
enabled: !!recipientAddress && !transferResult && isActive,
|
|
77
|
+
onTransferDetected: handleTransferDetected,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Fix 2: guard qrValue when recipientAddress is empty
|
|
81
|
+
const qrValue = useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
recipientAddress
|
|
84
|
+
? getPaymentUrl(
|
|
85
|
+
recipientAddress,
|
|
86
|
+
undefined,
|
|
87
|
+
token.address === ZERO_ADDRESS ? "ETH" : token.address,
|
|
88
|
+
chainId,
|
|
89
|
+
token.decimals,
|
|
90
|
+
)
|
|
91
|
+
: "",
|
|
92
|
+
[recipientAddress, token.address, token.decimals, chainId],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const chainName = useMemo(() => {
|
|
96
|
+
try {
|
|
97
|
+
return getChainName(chainId);
|
|
98
|
+
} catch {
|
|
99
|
+
return "the specified chain";
|
|
100
|
+
}
|
|
101
|
+
}, [chainId]);
|
|
102
|
+
|
|
103
|
+
// Guard: if no address yet, render nothing
|
|
104
|
+
if (!recipientAddress) return null;
|
|
105
|
+
|
|
106
|
+
// Once a transfer is detected, hand off to the shared success screen
|
|
107
|
+
if (transferResult) {
|
|
108
|
+
return (
|
|
109
|
+
<TransferResultScreen
|
|
110
|
+
mode={mode}
|
|
111
|
+
transferResult={transferResult}
|
|
112
|
+
token={token}
|
|
113
|
+
chainId={chainId}
|
|
114
|
+
recipientAddress={recipientAddress}
|
|
115
|
+
onBack={onBack}
|
|
116
|
+
onClose={onClose}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"anyspend-container anyspend-pure-transfer font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6",
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<div className="anyspend-pure-transfer-content flex flex-col gap-4">
|
|
128
|
+
{/* Header */}
|
|
129
|
+
<div className="anyspend-pure-transfer-header flex items-center justify-between">
|
|
130
|
+
{onBack ? (
|
|
131
|
+
<button
|
|
132
|
+
onClick={onBack}
|
|
133
|
+
className="anyspend-pure-transfer-back text-as-secondary hover:text-as-primary"
|
|
134
|
+
aria-label="Go back"
|
|
135
|
+
>
|
|
136
|
+
<ChevronLeft className="h-5 w-5" />
|
|
137
|
+
</button>
|
|
138
|
+
) : (
|
|
139
|
+
<div className="w-5" />
|
|
140
|
+
)}
|
|
141
|
+
<h2 className="anyspend-pure-transfer-title text-as-primary text-base font-semibold">Send {token.symbol}</h2>
|
|
142
|
+
{onClose ? (
|
|
143
|
+
<button
|
|
144
|
+
onClick={onClose}
|
|
145
|
+
className="anyspend-pure-transfer-close text-as-secondary hover:text-as-primary"
|
|
146
|
+
aria-label="Close"
|
|
147
|
+
>
|
|
148
|
+
<X className="h-5 w-5" />
|
|
149
|
+
</button>
|
|
150
|
+
) : (
|
|
151
|
+
<div className="w-5" />
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Optional amount badge */}
|
|
156
|
+
{displayAmount && (
|
|
157
|
+
<div className="anyspend-pure-transfer-amount-badge border-as-border-secondary bg-as-surface-secondary flex items-center justify-center rounded-xl border px-4 py-2">
|
|
158
|
+
<span className="text-as-primary text-sm font-medium">{displayAmount}</span>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{/* QR Code and address block — mirrors QRDeposit ~lines 413-453 */}
|
|
163
|
+
<div className="anyspend-pure-transfer-qr-content border-as-stroke flex items-start gap-4 rounded-xl border p-4">
|
|
164
|
+
{/* QR Code */}
|
|
165
|
+
<div className="anyspend-pure-transfer-qr-code-container flex flex-col items-center gap-2">
|
|
166
|
+
<div className="anyspend-pure-transfer-qr-code rounded-lg bg-white p-2">
|
|
167
|
+
<QRCodeSVG value={qrValue} size={120} level="M" marginSize={0} />
|
|
168
|
+
</div>
|
|
169
|
+
<span className="anyspend-pure-transfer-qr-scan-hint text-as-secondary text-xs">
|
|
170
|
+
SCAN WITH <span className="inline-block">🦊</span>
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Address info */}
|
|
175
|
+
<div className="anyspend-pure-transfer-address-container flex flex-1 flex-col gap-1">
|
|
176
|
+
<span className="anyspend-pure-transfer-address-label text-as-secondary text-sm">Deposit address:</span>
|
|
177
|
+
<div className="anyspend-pure-transfer-address-row flex items-start gap-1">
|
|
178
|
+
<span className="anyspend-pure-transfer-address text-as-primary break-all font-mono text-sm leading-relaxed">
|
|
179
|
+
{recipientAddress}
|
|
180
|
+
</span>
|
|
181
|
+
<CopyToClipboard
|
|
182
|
+
text={recipientAddress}
|
|
183
|
+
className="anyspend-pure-transfer-copy-icon text-as-secondary hover:text-as-primary mt-0.5 shrink-0"
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Warnings — mirrors QRDeposit ~lines 456-460 */}
|
|
190
|
+
<ChainWarningText chainId={chainId} />
|
|
191
|
+
<WarningText>
|
|
192
|
+
Only send {token.symbol} on {chainName}. Other tokens will not be converted.
|
|
193
|
+
</WarningText>
|
|
194
|
+
|
|
195
|
+
{/* Watching indicator — mirrors QRDeposit ~lines 462-473 */}
|
|
196
|
+
{isWatching && (
|
|
197
|
+
<div className="anyspend-pure-transfer-watching bg-as-brand/10 flex items-center justify-center gap-2 rounded-lg p-3">
|
|
198
|
+
<Loader2 className="text-as-brand h-4 w-4 animate-spin" />
|
|
199
|
+
<span className="text-as-brand text-sm">Watching for incoming transfer...</span>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Full-width copy button — mirrors QRDeposit ~lines 476-484 */}
|
|
204
|
+
<CopyToClipboard text={recipientAddress}>
|
|
205
|
+
<button className="anyspend-pure-transfer-copy-button bg-as-brand hover:bg-as-brand/90 flex w-full items-center justify-center gap-2 rounded-xl py-3.5 font-medium text-white transition-all">
|
|
206
|
+
Copy deposit address
|
|
207
|
+
</button>
|
|
208
|
+
</CopyToClipboard>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|