@reown/appkit-pay
Version:
465 lines • 17.4 kB
JavaScript
import { proxy, subscribe as sub } from 'valtio/vanilla';
import { subscribeKey as subKey } from 'valtio/vanilla/utils';
import { ConstantsUtil, ParseUtil } from '@reown/appkit-common';
import { AccountController, ChainController, CoreHelperUtil, EventsController, ModalController, RouterController, SnackController } from '@reown/appkit-controllers';
import { ProviderUtil } from '@reown/appkit-utils';
import { AppKitPayErrorCodes, AppKitPayErrorMessages } from '../types/errors.js';
import { AppKitPayError } from '../types/errors.js';
import { getBuyStatus, getExchanges, getPayUrl } from '../utils/ApiUtil.js';
import { formatCaip19Asset } from '../utils/AssetUtil.js';
import { ensureCorrectNetwork, processEvmErc20Payment, processEvmNativePayment } from '../utils/PaymentUtil.js';
const DEFAULT_PAGE = 0;
const DEFAULT_PAYMENT_ID = 'unknown';
const state = proxy({
paymentAsset: {
network: 'eip155:1',
asset: '0x0',
metadata: {
name: '0x0',
symbol: '0x0',
decimals: 0
}
},
recipient: '0x0',
amount: 0,
isConfigured: false,
error: null,
isPaymentInProgress: false,
exchanges: [],
isLoading: false,
openInNewTab: true,
redirectUrl: undefined,
payWithExchange: undefined,
currentPayment: undefined,
analyticsSet: false,
paymentId: undefined
});
export const PayController = {
state,
subscribe(callback) {
return sub(state, () => callback(state));
},
subscribeKey(key, callback) {
return subKey(state, key, callback);
},
async handleOpenPay(options) {
this.resetState();
this.setPaymentConfig(options);
this.subscribeEvents();
this.initializeAnalytics();
state.isConfigured = true;
EventsController.sendEvent({
type: 'track',
event: 'PAY_MODAL_OPEN',
properties: {
exchanges: state.exchanges,
configuration: {
network: state.paymentAsset.network,
asset: state.paymentAsset.asset,
recipient: state.recipient,
amount: state.amount
}
}
});
await ModalController.open({
view: 'Pay'
});
},
resetState() {
state.paymentAsset = {
network: 'eip155:1',
asset: '0x0',
metadata: { name: '0x0', symbol: '0x0', decimals: 0 }
};
state.recipient = '0x0';
state.amount = 0;
state.isConfigured = false;
state.error = null;
state.isPaymentInProgress = false;
state.isLoading = false;
state.currentPayment = undefined;
},
setPaymentConfig(config) {
if (!config.paymentAsset) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_PAYMENT_CONFIG);
}
try {
state.paymentAsset = config.paymentAsset;
state.recipient = config.recipient;
state.amount = config.amount;
state.openInNewTab = config.openInNewTab ?? true;
state.redirectUrl = config.redirectUrl;
state.payWithExchange = config.payWithExchange;
state.error = null;
}
catch (error) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_PAYMENT_CONFIG, error.message);
}
},
getPaymentAsset() {
return state.paymentAsset;
},
getExchanges() {
return state.exchanges;
},
async fetchExchanges() {
try {
state.isLoading = true;
const response = await getExchanges({
page: DEFAULT_PAGE,
asset: formatCaip19Asset(state.paymentAsset.network, state.paymentAsset.asset),
amount: state.amount.toString()
});
state.exchanges = response.exchanges.slice(0, 2);
}
catch (error) {
SnackController.showError(AppKitPayErrorMessages.UNABLE_TO_GET_EXCHANGES);
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_EXCHANGES);
}
finally {
state.isLoading = false;
}
},
async getAvailableExchanges(params) {
try {
const asset = params?.asset && params?.network
? formatCaip19Asset(params.network, params.asset)
: undefined;
const response = await getExchanges({
page: params?.page ?? DEFAULT_PAGE,
asset,
amount: params?.amount?.toString()
});
return response;
}
catch (error) {
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_EXCHANGES);
}
},
async getPayUrl(exchangeId, params, headless = false) {
try {
const numericAmount = Number(params.amount);
const response = await getPayUrl({
exchangeId,
asset: formatCaip19Asset(params.network, params.asset),
amount: numericAmount.toString(),
recipient: `${params.network}:${params.recipient}`
});
EventsController.sendEvent({
type: 'track',
event: 'PAY_EXCHANGE_SELECTED',
properties: {
exchange: {
id: exchangeId
},
configuration: {
network: params.network,
asset: params.asset,
recipient: params.recipient,
amount: numericAmount
},
currentPayment: {
type: 'exchange',
exchangeId
},
headless
}
});
if (headless) {
this.initiatePayment();
EventsController.sendEvent({
type: 'track',
event: 'PAY_INITIATED',
properties: {
paymentId: state.paymentId || DEFAULT_PAYMENT_ID,
configuration: {
network: params.network,
asset: params.asset,
recipient: params.recipient,
amount: numericAmount
},
currentPayment: {
type: 'exchange',
exchangeId
}
}
});
}
return response;
}
catch (error) {
if (error instanceof Error && error.message.includes('is not supported')) {
throw new AppKitPayError(AppKitPayErrorCodes.ASSET_NOT_SUPPORTED);
}
throw new Error(error.message);
}
},
async openPayUrl(openParams, params, headless = false) {
try {
const payUrl = await this.getPayUrl(openParams.exchangeId, params, headless);
if (!payUrl) {
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_PAY_URL);
}
const shouldOpenInNewTab = openParams.openInNewTab ?? true;
const target = shouldOpenInNewTab ? '_blank' : '_self';
CoreHelperUtil.openHref(payUrl.url, target);
return payUrl;
}
catch (error) {
if (error instanceof AppKitPayError) {
state.error = error.message;
}
else {
state.error = AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR;
}
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_PAY_URL);
}
},
subscribeEvents() {
if (state.isConfigured) {
return;
}
ProviderUtil.subscribeProviders(async (_) => {
const chainNamespace = ChainController.state.activeChain;
const provider = ProviderUtil.getProvider(chainNamespace);
if (!provider) {
return;
}
await this.handlePayment();
});
AccountController.subscribeKey('caipAddress', async (caipAddress) => {
if (!caipAddress) {
return;
}
await this.handlePayment();
});
},
async handlePayment() {
state.currentPayment = {
type: 'wallet',
status: 'IN_PROGRESS'
};
const caipAddress = AccountController.state.caipAddress;
if (!caipAddress) {
return;
}
const { chainId, address } = ParseUtil.parseCaipAddress(caipAddress);
const chainNamespace = ChainController.state.activeChain;
if (!address || !chainId || !chainNamespace) {
return;
}
const provider = ProviderUtil.getProvider(chainNamespace);
if (!provider) {
return;
}
const caipNetwork = ChainController.state.activeCaipNetwork;
if (!caipNetwork) {
return;
}
if (state.isPaymentInProgress) {
return;
}
try {
this.initiatePayment();
const requestedCaipNetworks = ChainController.getAllRequestedCaipNetworks();
const approvedCaipNetworkIds = ChainController.getAllApprovedCaipNetworkIds();
await ensureCorrectNetwork({
paymentAssetNetwork: state.paymentAsset.network,
activeCaipNetwork: caipNetwork,
approvedCaipNetworkIds,
requestedCaipNetworks
});
await ModalController.open({
view: 'PayLoading'
});
switch (chainNamespace) {
case ConstantsUtil.CHAIN.EVM:
if (state.paymentAsset.asset === 'native') {
state.currentPayment.result = await processEvmNativePayment(state.paymentAsset, chainNamespace, {
recipient: state.recipient,
amount: state.amount,
fromAddress: address
});
}
if (state.paymentAsset.asset.startsWith('0x')) {
state.currentPayment.result = await processEvmErc20Payment(state.paymentAsset, {
recipient: state.recipient,
amount: state.amount,
fromAddress: address
});
}
state.currentPayment.status = 'SUCCESS';
break;
default:
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_CHAIN_NAMESPACE);
}
}
catch (error) {
if (error instanceof AppKitPayError) {
state.error = error.message;
}
else {
state.error = AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR;
}
state.currentPayment.status = 'FAILED';
SnackController.showError(state.error);
}
finally {
state.isPaymentInProgress = false;
}
},
getExchangeById(exchangeId) {
return state.exchanges.find(exchange => exchange.id === exchangeId);
},
validatePayConfig(config) {
const { paymentAsset, recipient, amount } = config;
if (!paymentAsset) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_PAYMENT_CONFIG);
}
if (!recipient) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_RECIPIENT);
}
if (!paymentAsset.asset) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_ASSET);
}
if (amount === undefined || amount === null || amount <= 0) {
throw new AppKitPayError(AppKitPayErrorCodes.INVALID_AMOUNT);
}
},
handlePayWithWallet() {
const caipAddress = AccountController.state.caipAddress;
if (!caipAddress) {
RouterController.push('Connect');
return;
}
const { chainId, address } = ParseUtil.parseCaipAddress(caipAddress);
const chainNamespace = ChainController.state.activeChain;
if (!address || !chainId || !chainNamespace) {
RouterController.push('Connect');
return;
}
this.handlePayment();
},
async handlePayWithExchange(exchangeId) {
try {
state.currentPayment = {
type: 'exchange',
exchangeId
};
const { network, asset } = state.paymentAsset;
const payUrlParams = {
network,
asset,
amount: state.amount,
recipient: state.recipient
};
const payUrl = await this.getPayUrl(exchangeId, payUrlParams);
if (!payUrl) {
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_INITIATE_PAYMENT);
}
state.currentPayment.sessionId = payUrl.sessionId;
state.currentPayment.status = 'IN_PROGRESS';
state.currentPayment.exchangeId = exchangeId;
this.initiatePayment();
return {
url: payUrl.url,
openInNewTab: state.openInNewTab
};
}
catch (error) {
if (error instanceof AppKitPayError) {
state.error = error.message;
}
else {
state.error = AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR;
}
state.isPaymentInProgress = false;
SnackController.showError(state.error);
return null;
}
},
async getBuyStatus(exchangeId, sessionId) {
try {
const status = await getBuyStatus({ sessionId, exchangeId });
if (status.status === 'SUCCESS' || status.status === 'FAILED') {
EventsController.sendEvent({
type: 'track',
event: status.status === 'SUCCESS' ? 'PAY_SUCCESS' : 'PAY_ERROR',
properties: {
paymentId: state.paymentId || DEFAULT_PAYMENT_ID,
configuration: {
network: state.paymentAsset.network,
asset: state.paymentAsset.asset,
recipient: state.recipient,
amount: state.amount
},
currentPayment: {
type: 'exchange',
exchangeId: state.currentPayment?.exchangeId,
sessionId: state.currentPayment?.sessionId,
result: status.txHash
}
}
});
}
return status;
}
catch (error) {
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_BUY_STATUS);
}
},
async updateBuyStatus(exchangeId, sessionId) {
try {
const status = await this.getBuyStatus(exchangeId, sessionId);
if (state.currentPayment) {
state.currentPayment.status = status.status;
state.currentPayment.result = status.txHash;
}
if (status.status === 'SUCCESS' || status.status === 'FAILED') {
state.isPaymentInProgress = false;
}
}
catch (error) {
throw new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_BUY_STATUS);
}
},
initiatePayment() {
state.isPaymentInProgress = true;
state.paymentId = crypto.randomUUID();
},
initializeAnalytics() {
if (state.analyticsSet) {
return;
}
state.analyticsSet = true;
this.subscribeKey('isPaymentInProgress', _ => {
if (state.currentPayment?.status && state.currentPayment.status !== 'UNKNOWN') {
const eventType = {
IN_PROGRESS: 'PAY_INITIATED',
SUCCESS: 'PAY_SUCCESS',
FAILED: 'PAY_ERROR'
}[state.currentPayment.status];
EventsController.sendEvent({
type: 'track',
event: eventType,
properties: {
paymentId: state.paymentId || DEFAULT_PAYMENT_ID,
configuration: {
network: state.paymentAsset.network,
asset: state.paymentAsset.asset,
recipient: state.recipient,
amount: state.amount
},
currentPayment: {
type: state.currentPayment.type,
exchangeId: state.currentPayment.exchangeId,
sessionId: state.currentPayment.sessionId,
result: state.currentPayment.result
}
}
});
}
});
}
};
//# sourceMappingURL=PayController.js.map