@reown/appkit-pay
Version:
428 lines • 23.1 kB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConstantsUtil, ParseUtil } from '@reown/appkit-common';
import { AccountController, ChainController, CoreHelperUtil, ModalController, RouterController, SnackController } from '@reown/appkit-controllers';
import { ProviderUtil } from '@reown/appkit-utils';
import { PayController } from '../../src/controllers/PayController';
import { AppKitPayError, AppKitPayErrorCodes, AppKitPayErrorMessages } from '../../src/types/errors';
import * as ApiUtil from '../../src/utils/ApiUtil';
import * as AssetUtil from '../../src/utils/AssetUtil';
import * as PaymentUtil from '../../src/utils/PaymentUtil';
describe('PayController', () => {
const mockPaymentOptions = {
paymentAsset: {
network: 'eip155:1',
recipient: '0x1234567890123456789012345678901234567890',
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
amount: 10,
metadata: {
name: 'USD Coin',
symbol: 'USDC',
decimals: 6
}
},
openInNewTab: true,
redirectUrl: {
success: 'https://example.com',
failure: 'https://example.com'
},
payWithExchange: 'coinbase'
};
const mockExchanges = [
{
id: 'coinbase',
name: 'Coinbase',
imageUrl: 'https://example.com/icon.png'
},
{
id: 'binance',
name: 'Binance',
imageUrl: 'https://example.com/icon.png'
}
];
const mockExchangesResponse = {
exchanges: mockExchanges,
total: 2
};
const mockPayUrlResponse = {
url: 'https://exchange.com/buy?asset=eth&amount=10',
sessionId: '123'
};
beforeEach(() => {
vi.resetAllMocks();
PayController.state.isConfigured = false;
PayController.state.error = null;
PayController.state.isPaymentInProgress = false;
PayController.state.isLoading = false;
PayController.state.exchanges = [];
Object.defineProperty(ChainController.state, 'activeChain', {
get: vi.fn(() => ConstantsUtil.CHAIN.EVM),
configurable: true
});
Object.defineProperty(ChainController.state, 'activeCaipNetwork', {
get: vi.fn(() => ({
caipNetworkId: 'eip155:1'
})),
configurable: true
});
Object.defineProperty(AccountController.state, 'caipAddress', {
get: vi.fn(() => 'eip155:1:0x1234567890123456789012345678901234567890'),
configurable: true
});
vi.spyOn(ModalController, 'open').mockResolvedValue(undefined);
vi.spyOn(RouterController, 'push').mockImplementation(() => { });
vi.spyOn(SnackController, 'showError').mockImplementation(() => { });
vi.spyOn(ApiUtil, 'getExchanges').mockResolvedValue(mockExchangesResponse);
vi.spyOn(ApiUtil, 'getPayUrl').mockResolvedValue(mockPayUrlResponse);
vi.spyOn(ProviderUtil, 'subscribeProviders').mockImplementation(callback => {
callback({});
return () => { };
});
vi.spyOn(ProviderUtil, 'getProvider').mockReturnValue({});
vi.spyOn(ParseUtil, 'parseCaipAddress').mockReturnValue({
chainId: '1',
address: '0x1234567890123456789012345678901234567890',
chainNamespace: 'eip155'
});
vi.spyOn(PaymentUtil, 'ensureCorrectNetwork').mockResolvedValue(undefined);
vi.spyOn(PaymentUtil, 'processEvmNativePayment').mockResolvedValue(undefined);
vi.spyOn(PaymentUtil, 'processEvmErc20Payment').mockResolvedValue(undefined);
vi.spyOn(AssetUtil, 'formatCaip19Asset').mockReturnValue('eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48');
vi.spyOn(CoreHelperUtil, 'openHref').mockImplementation(() => { });
});
describe('setPaymentConfig', () => {
it('should set payment config correctly', () => {
PayController.setPaymentConfig(mockPaymentOptions);
expect(PayController.state.paymentAsset).toEqual(mockPaymentOptions.paymentAsset);
expect(PayController.state.openInNewTab).toEqual(mockPaymentOptions.openInNewTab);
expect(PayController.state.redirectUrl).toEqual(mockPaymentOptions.redirectUrl);
expect(PayController.state.payWithExchange).toEqual(mockPaymentOptions.payWithExchange);
expect(PayController.state.error).toBeNull();
});
it('should throw error for invalid payment config', () => {
const mockInvalidConfig = {};
expect(() => PayController.setPaymentConfig(mockInvalidConfig)).toThrow(AppKitPayError);
});
});
describe('handleOpenPay', () => {
it('should configure payment and open modal', async () => {
const setPaymentConfigSpy = vi.spyOn(PayController, 'setPaymentConfig');
const subscribeEventsSpy = vi.spyOn(PayController, 'subscribeEvents');
await PayController.handleOpenPay(mockPaymentOptions);
expect(setPaymentConfigSpy).toHaveBeenCalledWith(mockPaymentOptions);
expect(subscribeEventsSpy).toHaveBeenCalled();
expect(PayController.state.isConfigured).toBe(true);
expect(ModalController.open).toHaveBeenCalledWith({ view: 'Pay' });
});
});
describe('getPaymentAsset', () => {
it('should return payment asset from state', () => {
PayController.state.paymentAsset = mockPaymentOptions.paymentAsset;
const result = PayController.getPaymentAsset();
expect(result).toEqual(mockPaymentOptions.paymentAsset);
});
});
describe('fetchExchanges', () => {
it('should fetch and set exchanges in state', async () => {
await PayController.fetchExchanges();
expect(ApiUtil.getExchanges).toHaveBeenCalledWith({ page: 0 });
expect(PayController.state.exchanges).toEqual(mockExchanges.slice(0, 2));
expect(PayController.state.isLoading).toBe(false);
});
it('should set isLoading to false even if fetch fails', async () => {
vi.spyOn(ApiUtil, 'getExchanges').mockRejectedValueOnce(new Error('API error'));
await expect(PayController.fetchExchanges()).rejects.toThrow('Unable to get exchanges');
expect(PayController.state.isLoading).toBe(false);
});
});
describe('getPayUrl', () => {
it('should return pay URL from API', async () => {
const params = {
network: mockPaymentOptions.paymentAsset.network,
asset: mockPaymentOptions.paymentAsset.asset,
amount: mockPaymentOptions.paymentAsset.amount,
recipient: mockPaymentOptions.paymentAsset.recipient
};
const result = await PayController.getPayUrl('coinbase', params);
expect(ApiUtil.getPayUrl).toHaveBeenCalledWith({
exchangeId: 'coinbase',
asset: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
amount: 'a',
recipient: 'eip155:1:0x1234567890123456789012345678901234567890'
});
expect(result).toEqual(mockPayUrlResponse);
});
it('should handle API errors', async () => {
vi.spyOn(ApiUtil, 'getPayUrl').mockRejectedValueOnce(new Error('API error'));
const params = {
network: mockPaymentOptions.paymentAsset.network,
asset: mockPaymentOptions.paymentAsset.asset,
amount: mockPaymentOptions.paymentAsset.amount,
recipient: mockPaymentOptions.paymentAsset.recipient
};
await expect(PayController.getPayUrl('coinbase', params)).rejects.toThrow('API error');
});
it('should handle asset not supported error', async () => {
vi.spyOn(ApiUtil, 'getPayUrl').mockRejectedValueOnce(new Error('Asset is not supported by the selected exchange'));
const params = {
network: mockPaymentOptions.paymentAsset.network,
asset: mockPaymentOptions.paymentAsset.asset,
amount: mockPaymentOptions.paymentAsset.amount,
recipient: mockPaymentOptions.paymentAsset.recipient
};
await expect(PayController.getPayUrl('coinbase', params)).rejects.toThrow(new AppKitPayError(AppKitPayErrorCodes.ASSET_NOT_SUPPORTED));
});
});
describe('handlePayment', () => {
it('should handle EVM native token payment', async () => {
PayController.state.paymentAsset = {
...mockPaymentOptions.paymentAsset,
asset: 'native'
};
await PayController.handlePayment();
expect(PaymentUtil.processEvmNativePayment).toHaveBeenCalledWith(PayController.state.paymentAsset, ConstantsUtil.CHAIN.EVM, '0x1234567890123456789012345678901234567890');
expect(ModalController.open).toHaveBeenCalledWith({ view: 'PayLoading' });
});
it('should handle EVM ERC20 token payment', async () => {
PayController.state.paymentAsset = {
...mockPaymentOptions.paymentAsset,
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
};
await PayController.handlePayment();
expect(PaymentUtil.processEvmErc20Payment).toHaveBeenCalledWith(PayController.state.paymentAsset, '0x1234567890123456789012345678901234567890');
});
it('should handle payment processing errors', async () => {
vi.spyOn(PaymentUtil, 'processEvmErc20Payment').mockRejectedValueOnce(new Error('Payment error'));
await PayController.handlePayment();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR);
expect(SnackController.showError).toHaveBeenCalled();
});
it('should throw error for unsupported chain namespace', async () => {
Object.defineProperty(ChainController.state, 'activeChain', {
get: vi.fn(() => 'unsupported'),
configurable: true
});
await PayController.handlePayment();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.INVALID_CHAIN_NAMESPACE);
});
});
describe('validatePayConfig', () => {
it('should not throw error for valid config', () => {
expect(() => PayController.validatePayConfig(mockPaymentOptions)).not.toThrow();
});
it('should throw error for missing payment asset', () => {
const invalidConfig = { ...mockPaymentOptions, paymentAsset: undefined };
expect(() => PayController.validatePayConfig(invalidConfig)).toThrow(new AppKitPayError(AppKitPayErrorCodes.INVALID_PAYMENT_CONFIG));
});
it('should throw error for missing recipient', () => {
const invalidConfig = {
...mockPaymentOptions,
paymentAsset: { ...mockPaymentOptions.paymentAsset, recipient: undefined }
};
expect(() => PayController.validatePayConfig(invalidConfig)).toThrow(new AppKitPayError(AppKitPayErrorCodes.INVALID_RECIPIENT));
});
it('should throw error for missing asset', () => {
const invalidConfig = {
...mockPaymentOptions,
paymentAsset: { ...mockPaymentOptions.paymentAsset, asset: undefined }
};
expect(() => PayController.validatePayConfig(invalidConfig)).toThrow(new AppKitPayError(AppKitPayErrorCodes.INVALID_ASSET));
});
it('should throw error for missing amount', () => {
const invalidConfig = {
...mockPaymentOptions,
paymentAsset: { ...mockPaymentOptions.paymentAsset, amount: undefined }
};
expect(() => PayController.validatePayConfig(invalidConfig)).toThrow(new AppKitPayError(AppKitPayErrorCodes.INVALID_AMOUNT));
});
});
describe('handlePayWithWallet', () => {
it('should redirect to Connect if no caipAddress', () => {
Object.defineProperty(AccountController.state, 'caipAddress', {
get: vi.fn(() => null),
configurable: true
});
PayController.handlePayWithWallet();
expect(RouterController.push).toHaveBeenCalledWith('Connect');
});
it('should redirect to Connect if parseCaipAddress returns incomplete data', () => {
vi.spyOn(ParseUtil, 'parseCaipAddress').mockReturnValueOnce({
chainId: null,
address: null,
chainNamespace: null
});
PayController.handlePayWithWallet();
expect(RouterController.push).toHaveBeenCalledWith('Connect');
});
it('should call handlePayment if user is connected', () => {
const handlePaymentSpy = vi
.spyOn(PayController, 'handlePayment')
.mockImplementation(async () => { });
PayController.handlePayWithWallet();
expect(handlePaymentSpy).toHaveBeenCalled();
});
});
describe('handlePayWithExchange', () => {
it('should get pay URL and return object for opening in new tab', async () => {
PayController.state.openInNewTab = true;
PayController.setPaymentConfig(mockPaymentOptions);
const getPayUrlSpy = vi.spyOn(PayController, 'getPayUrl');
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
const result = await PayController.handlePayWithExchange('coinbase');
expect(getPayUrlSpy).toHaveBeenCalledWith('coinbase', {
network: mockPaymentOptions.paymentAsset.network,
asset: mockPaymentOptions.paymentAsset.asset,
amount: mockPaymentOptions.paymentAsset.amount,
recipient: mockPaymentOptions.paymentAsset.recipient
});
expect(PayController.state.isPaymentInProgress).toBe(true);
expect(PayController.state.currentPayment).toEqual({
type: 'exchange',
exchangeId: 'coinbase',
sessionId: mockPayUrlResponse.sessionId,
status: 'IN_PROGRESS'
});
expect(result).toEqual({
url: mockPayUrlResponse.url,
openInNewTab: true
});
expect(ModalController.open).not.toHaveBeenCalled();
expect(openHrefSpy).not.toHaveBeenCalled();
});
it('should get pay URL and return object for opening in same tab', async () => {
PayController.setPaymentConfig(mockPaymentOptions);
PayController.state.openInNewTab = false;
const getPayUrlSpy = vi
.spyOn(PayController, 'getPayUrl')
.mockResolvedValue(mockPayUrlResponse);
const routerPushSpy = vi.spyOn(RouterController, 'push');
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
const snackErrorSpy = vi.spyOn(SnackController, 'showError');
const result = await PayController.handlePayWithExchange('coinbase');
expect(getPayUrlSpy).toHaveBeenCalled();
expect(PayController.state.isPaymentInProgress).toBe(true);
expect(PayController.state.currentPayment).toEqual({
type: 'exchange',
exchangeId: 'coinbase',
sessionId: mockPayUrlResponse.sessionId,
status: 'IN_PROGRESS'
});
expect(result).toEqual({
url: mockPayUrlResponse.url,
openInNewTab: false
});
expect(snackErrorSpy).not.toHaveBeenCalled();
expect(routerPushSpy).not.toHaveBeenCalled();
expect(openHrefSpy).not.toHaveBeenCalled();
});
it('should set error state and show snackbar if unable to get pay URL', async () => {
PayController.setPaymentConfig(mockPaymentOptions);
vi.spyOn(PayController, 'getPayUrl').mockResolvedValue(null);
vi.spyOn(SnackController, 'showError').mockImplementation(() => { });
const result = await PayController.handlePayWithExchange('coinbase');
expect(result).toBeNull();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.UNABLE_TO_INITIATE_PAYMENT);
expect(PayController.state.isPaymentInProgress).toBe(false);
expect(SnackController.showError).toHaveBeenCalledWith(AppKitPayErrorMessages.UNABLE_TO_INITIATE_PAYMENT);
});
it('should handle generic error during exchange payment', async () => {
PayController.setPaymentConfig(mockPaymentOptions);
const genericError = new Error('Generic Error');
vi.spyOn(PayController, 'getPayUrl').mockRejectedValueOnce(genericError);
vi.spyOn(SnackController, 'showError').mockImplementation(() => { });
await PayController.handlePayWithExchange('coinbase');
expect(PayController.state.error).toBe(AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR);
expect(SnackController.showError).toHaveBeenCalledWith(AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR);
});
});
describe('openPayUrl', () => {
const params = {
network: mockPaymentOptions.paymentAsset.network,
asset: mockPaymentOptions.paymentAsset.asset,
amount: mockPaymentOptions.paymentAsset.amount,
recipient: mockPaymentOptions.paymentAsset.recipient
};
const exchangeId = 'coinbase';
const mockUrl = mockPayUrlResponse.url;
it('should get pay URL and open it in new tab by default', async () => {
const getPayUrlSpy = vi
.spyOn(PayController, 'getPayUrl')
.mockResolvedValue(mockPayUrlResponse);
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
await PayController.openPayUrl(exchangeId, params);
expect(getPayUrlSpy).toHaveBeenCalledWith(exchangeId, params);
expect(openHrefSpy).toHaveBeenCalledWith(mockUrl, '_blank');
expect(SnackController.showError).not.toHaveBeenCalled();
});
it('should get pay URL and open it in same tab when openInNewTab is false', async () => {
const getPayUrlSpy = vi
.spyOn(PayController, 'getPayUrl')
.mockResolvedValue(mockPayUrlResponse);
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
await PayController.openPayUrl(exchangeId, params, false);
expect(getPayUrlSpy).toHaveBeenCalledWith(exchangeId, params);
expect(openHrefSpy).toHaveBeenCalledWith(mockUrl, '_self');
expect(SnackController.showError).not.toHaveBeenCalled();
});
it('should handle error if getPayUrl returns null', async () => {
const getPayUrlSpy = vi.spyOn(PayController, 'getPayUrl').mockResolvedValue(null);
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
await expect(PayController.openPayUrl(exchangeId, params)).rejects.toThrow(new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_PAY_URL));
expect(getPayUrlSpy).toHaveBeenCalledWith(exchangeId, params);
expect(openHrefSpy).not.toHaveBeenCalled();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.UNABLE_TO_GET_PAY_URL);
});
it('should handle AppKitPayError from getPayUrl', async () => {
const originalError = new AppKitPayError(AppKitPayErrorCodes.ASSET_NOT_SUPPORTED);
const getPayUrlSpy = vi.spyOn(PayController, 'getPayUrl').mockRejectedValue(originalError);
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
await expect(PayController.openPayUrl(exchangeId, params)).rejects.toThrow(new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_PAY_URL));
expect(getPayUrlSpy).toHaveBeenCalledWith(exchangeId, params);
expect(openHrefSpy).not.toHaveBeenCalled();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.ASSET_NOT_SUPPORTED);
});
it('should handle generic error from getPayUrl', async () => {
const originalError = new Error('Generic network error');
const getPayUrlSpy = vi.spyOn(PayController, 'getPayUrl').mockRejectedValue(originalError);
const openHrefSpy = vi.spyOn(CoreHelperUtil, 'openHref');
await expect(PayController.openPayUrl(exchangeId, params)).rejects.toThrow(new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_PAY_URL));
expect(getPayUrlSpy).toHaveBeenCalledWith(exchangeId, params);
expect(openHrefSpy).not.toHaveBeenCalled();
expect(PayController.state.error).toBe(AppKitPayErrorMessages.GENERIC_PAYMENT_ERROR);
});
});
describe('subscribeEvents', () => {
it('should not subscribe if already configured', () => {
PayController.state.isConfigured = true;
const subscribeSpy = vi.spyOn(ProviderUtil, 'subscribeProviders');
PayController.subscribeEvents();
expect(subscribeSpy).not.toHaveBeenCalled();
});
});
describe('getExchanges', () => {
it('should return exchanges from state', () => {
PayController.state.exchanges = mockExchanges;
const result = PayController.getExchanges();
expect(result).toEqual(mockExchanges);
});
});
describe('getAvailableExchanges', () => {
it('should call getExchanges with default page 0 and return exchanges', async () => {
const result = await PayController.getAvailableExchanges();
expect(ApiUtil.getExchanges).toHaveBeenCalledWith({ page: 0 });
expect(result).toEqual(mockExchangesResponse);
});
it('should call getExchanges with the specified page number and return exchanges', async () => {
const page = 1;
const result = await PayController.getAvailableExchanges(page);
expect(ApiUtil.getExchanges).toHaveBeenCalledWith({ page });
expect(result).toEqual(mockExchangesResponse);
});
it('should throw AppKitPayError if getExchanges fails', async () => {
const apiError = new Error('API Error');
vi.spyOn(ApiUtil, 'getExchanges').mockRejectedValueOnce(apiError);
await expect(PayController.getAvailableExchanges()).rejects.toThrow(new AppKitPayError(AppKitPayErrorCodes.UNABLE_TO_GET_EXCHANGES));
expect(ApiUtil.getExchanges).toHaveBeenCalledWith({ page: 0 });
});
});
});
//# sourceMappingURL=PayController.test.js.map