Skip to content

Commit 1c1d677

Browse files
authored
Merge pull request #526 from PayButton/chore/switch-to-cashtab-connect
Switched to the new cashtab-connect lib
2 parents 349561d + bd904d2 commit 1c1d677

5 files changed

Lines changed: 146 additions & 28 deletions

File tree

react/lib/components/Widget/Widget.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
getAddressBalance,
1919
isFiat,
2020
Transaction,
21-
getCashtabProviderStatus,
21+
openCashtabPayment,
22+
initializeCashtabStatus,
2223
DECIMALS,
2324
CurrencyObject,
2425
getCurrencyObject,
@@ -415,6 +416,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
415416
const [text, setText] = useState(`Send any amount of ${thisAddressType}`);
416417
const [widgetButtonText, setWidgetButtonText] = useState('Send Payment');
417418
const [opReturn, setOpReturn] = useState<string | undefined>();
419+
const [isCashtabAvailable, setIsCashtabAvailable] = useState<boolean>(false);
418420

419421
const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState<boolean | null>(null);
420422

@@ -460,6 +462,19 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
460462
setHasPrice(price !== undefined && price > 0)
461463
}, [price])
462464

465+
useEffect(() => {
466+
const initCashtab = async () => {
467+
try {
468+
const isAvailable = await initializeCashtabStatus();
469+
setIsCashtabAvailable(isAvailable);
470+
} catch (error) {
471+
setIsCashtabAvailable(false);
472+
}
473+
};
474+
475+
initCashtab();
476+
}, []);
477+
463478
useEffect(() => {
464479
(async () => {
465480
if (isChild !== true) {
@@ -597,7 +612,12 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
597612
let url;
598613

599614
setThisAddressType(thisAddressType);
600-
setWidgetButtonText(`Send with ${thisAddressType} wallet`);
615+
616+
if (thisAddressType === 'XEC' && isCashtabAvailable) {
617+
setWidgetButtonText('Send with Cashtab');
618+
} else {
619+
setWidgetButtonText(`Send with ${thisAddressType} wallet`);
620+
}
601621

602622
if (thisCurrencyObject && hasPrice) {
603623
const convertedAmount = thisCurrencyObject.float / price
@@ -628,7 +648,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
628648
}
629649
setUrl(url ?? "");
630650
}
631-
}, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice]);
651+
}, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable]);
632652

633653
useEffect(() => {
634654
try {
@@ -693,24 +713,9 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
693713
}
694714
}, [totalReceived, currency, goalAmount, price, hasPrice, contributionOffset]);
695715

696-
const handleButtonClick = () => {
716+
const handleButtonClick = async () => {
697717
if (thisAddressType === 'XEC') {
698-
const hasExtension = getCashtabProviderStatus();
699-
if (!hasExtension) {
700-
const webUrl = `https://cashtab.com/#/send?bip21=${url}`;
701-
window.open(webUrl, '_blank');
702-
} else {
703-
return window.postMessage(
704-
{
705-
type: 'FROM_PAGE',
706-
text: 'Cashtab',
707-
txInfo: {
708-
bip21: url
709-
},
710-
},
711-
'*',
712-
);
713-
}
718+
await openCashtabPayment(url);
714719
} else {
715720
window.location.href = url;
716721
}

react/lib/util/api-client.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,3 @@ export default {
9797
getAddressBalance,
9898
};
9999

100-
export const getCashtabProviderStatus = () => {
101-
const windowAny = window as any
102-
if (window && windowAny.bitcoinAbc && windowAny.bitcoinAbc === 'cashtab') {
103-
return true;
104-
}
105-
return false;
106-
};
107-

react/lib/util/cashtab.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
CashtabConnect,
3+
CashtabExtensionUnavailableError,
4+
CashtabAddressDeniedError,
5+
CashtabTimeoutError
6+
} from 'cashtab-connect';
7+
8+
const cashtab = new CashtabConnect();
9+
10+
// Cache for extension status to avoid multiple checks
11+
let extensionStatusCache: boolean | null = null;
12+
let extensionStatusPromise: Promise<boolean> | null = null;
13+
14+
/**
15+
* Check if the Cashtab extension is available (with caching)
16+
* This function caches the result to avoid multiple extension checks per page load
17+
* @returns Promise<boolean> - true if extension is available, false otherwise
18+
*/
19+
export const getCashtabProviderStatus = async (): Promise<boolean> => {
20+
// Return cached result if available
21+
if (extensionStatusCache !== null) {
22+
return extensionStatusCache;
23+
}
24+
25+
// If a check is already in progress, wait for it
26+
if (extensionStatusPromise !== null) {
27+
return extensionStatusPromise;
28+
}
29+
30+
extensionStatusPromise = (async () => {
31+
try {
32+
const isAvailable = await cashtab.isExtensionAvailable();
33+
extensionStatusCache = isAvailable;
34+
return isAvailable;
35+
} catch (error) {
36+
extensionStatusCache = false;
37+
return false;
38+
} finally {
39+
// Clear the promise so future calls can make a fresh check if needed
40+
extensionStatusPromise = null;
41+
}
42+
})();
43+
44+
return extensionStatusPromise;
45+
};
46+
47+
/**
48+
* Clear the cached extension status (useful for testing or if extension state changes)
49+
*/
50+
export const clearCashtabStatusCache = (): void => {
51+
extensionStatusCache = null;
52+
extensionStatusPromise = null;
53+
};
54+
55+
56+
export const initializeCashtabStatus = async (): Promise<boolean> => {
57+
return getCashtabProviderStatus();
58+
};
59+
60+
export const waitForCashtabExtension = async (timeout?: number): Promise<void> => {
61+
return cashtab.waitForExtension(timeout);
62+
};
63+
64+
/**
65+
* Request the user's eCash address from their Cashtab wallet
66+
* @returns Promise<string> - The user's address
67+
* @throws {CashtabExtensionUnavailableError} When the Cashtab extension is not available
68+
* @throws {CashtabAddressDeniedError} When the user denies the address request
69+
* @throws {CashtabTimeoutError} When the request times out
70+
*/
71+
export const requestCashtabAddress = async (): Promise<string> => {
72+
return cashtab.requestAddress();
73+
};
74+
75+
export const sendXecWithCashtab = async (address: string, amount: string | number): Promise<any> => {
76+
return cashtab.sendXec(address, amount);
77+
};
78+
79+
/**
80+
* Open Cashtab with a BIP21 payment URL
81+
* @param bip21Url - The BIP21 formatted payment URL
82+
* @param fallbackUrl - Optional fallback URL if extension is not available
83+
*/
84+
export const openCashtabPayment = async (bip21Url: string, fallbackUrl?: string): Promise<void> => {
85+
try {
86+
const isAvailable = await getCashtabProviderStatus();
87+
88+
if (isAvailable) {
89+
const url = new URL(bip21Url);
90+
const address = url.pathname;
91+
const amount = url.searchParams.get('amount');
92+
93+
if (amount) {
94+
await sendXecWithCashtab(address, amount);
95+
} else {
96+
const webUrl = fallbackUrl || `https://cashtab.com/#/send?bip21=${encodeURIComponent(bip21Url)}`;
97+
window.open(webUrl, '_blank');
98+
}
99+
} else {
100+
const webUrl = fallbackUrl || `https://cashtab.com/#/send?bip21=${encodeURIComponent(bip21Url)}`;
101+
window.open(webUrl, '_blank');
102+
}
103+
} catch (error) {
104+
if (error instanceof CashtabAddressDeniedError) {
105+
// User rejected the transaction - do nothing for now
106+
// This case is handled here in case we want to add specific behavior in the future
107+
return;
108+
}
109+
110+
const webUrl = fallbackUrl || `https://cashtab.com/#/send?bip21=${encodeURIComponent(bip21Url)}`;
111+
window.open(webUrl, '_blank');
112+
}
113+
};
114+
115+
export {
116+
CashtabExtensionUnavailableError,
117+
CashtabAddressDeniedError,
118+
CashtabTimeoutError
119+
};

react/lib/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './address';
22
export * from './api-client';
3+
export * from './cashtab';
34
export * from './constants';
45
export * from './format';
56
export * from './opReturn';

react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@types/jest": "^29.5.11",
8989
"axios": "1.6.5",
9090
"bignumber.js": "9.0.2",
91+
"cashtab-connect": "^1.1.0",
9192
"copy-to-clipboard": "3.3.3",
9293
"crypto-js": "^4.2.0",
9394
"jest": "^29.7.0",

0 commit comments

Comments
 (0)