From 28100c7b4ab18902eae75be6829e9c943649ce07 Mon Sep 17 00:00:00 2001 From: "V. K." Date: Mon, 2 Mar 2026 19:04:41 +0400 Subject: [PATCH 01/67] feat(onramp): start with onramp --- .../src/api/interfaces/DefiProvider.ts | 2 +- .../walletkit/src/api/interfaces/OnrampAPI.ts | 101 +++++++ .../walletkit/src/api/interfaces/index.ts | 1 + packages/walletkit/src/api/models/index.ts | 12 + .../api/models/onramps/OnrampLimitParams.ts | 24 ++ .../src/api/models/onramps/OnrampLimits.ts | 37 +++ .../src/api/models/onramps/OnrampParams.ts | 30 ++ .../src/api/models/onramps/OnrampQuote.ts | 57 ++++ .../api/models/onramps/OnrampQuoteParams.ts | 45 +++ .../models/onramps/OnrampTransactionParams.ts | 19 ++ .../models/onramps/OnrampTransactionStatus.ts | 59 ++++ .../walletkit/src/api/models/onramps/index.ts | 15 + packages/walletkit/src/defi/index.ts | 12 + .../src/defi/onramp/OnrampManager.spec.ts | 141 ++++++++++ .../src/defi/onramp/OnrampManager.ts | 162 +++++++++++ .../src/defi/onramp/OnrampProvider.ts | 78 ++++++ packages/walletkit/src/defi/onramp/errors.ts | 21 ++ packages/walletkit/src/defi/onramp/index.ts | 12 + .../onramp/moonpay/MoonpayProvider.spec.ts | 265 ++++++++++++++++++ .../defi/onramp/moonpay/MoonpayProvider.ts | 207 ++++++++++++++ .../src/defi/onramp/moonpay/index.ts | 9 + 21 files changed, 1308 insertions(+), 1 deletion(-) create mode 100644 packages/walletkit/src/api/interfaces/OnrampAPI.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampLimits.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampParams.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampQuote.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampQuoteParams.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts create mode 100644 packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts create mode 100644 packages/walletkit/src/api/models/onramps/index.ts create mode 100644 packages/walletkit/src/defi/index.ts create mode 100644 packages/walletkit/src/defi/onramp/OnrampManager.spec.ts create mode 100644 packages/walletkit/src/defi/onramp/OnrampManager.ts create mode 100644 packages/walletkit/src/defi/onramp/OnrampProvider.ts create mode 100644 packages/walletkit/src/defi/onramp/errors.ts create mode 100644 packages/walletkit/src/defi/onramp/index.ts create mode 100644 packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts create mode 100644 packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts create mode 100644 packages/walletkit/src/defi/onramp/moonpay/index.ts diff --git a/packages/walletkit/src/api/interfaces/DefiProvider.ts b/packages/walletkit/src/api/interfaces/DefiProvider.ts index f1a327f21..38144b587 100644 --- a/packages/walletkit/src/api/interfaces/DefiProvider.ts +++ b/packages/walletkit/src/api/interfaces/DefiProvider.ts @@ -9,7 +9,7 @@ /** * Type of provider */ -export type DefiProviderType = 'swap'; +export type DefiProviderType = 'swap' | 'onramp'; /** * Base interface for all providers diff --git a/packages/walletkit/src/api/interfaces/OnrampAPI.ts b/packages/walletkit/src/api/interfaces/OnrampAPI.ts new file mode 100644 index 000000000..54ca9440c --- /dev/null +++ b/packages/walletkit/src/api/interfaces/OnrampAPI.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + OnrampParams, + OnrampQuote, + OnrampQuoteParams, + OnrampLimits, + OnrampLimitParams, + OnrampTransactionStatus, + OnrampTransactionParams, +} from '../models'; +import type { DefiManagerAPI } from './DefiManagerAPI'; +import type { DefiProvider } from './DefiProvider'; + +/** + * Onramp API interface exposed by OnrampManager + */ +export interface OnrampAPI extends DefiManagerAPI { + /** + * Get a quote for onramping fiat to crypto + * @param params Quote parameters (fiat, crypto, amount, etc.) + * @param providerId Provider identifier (optional, uses default if not specified) + * @returns A promise that resolves to an OnrampQuote + */ + getQuote(params: OnrampQuoteParams, providerId?: string): Promise; + + /** + * Get fiat/crypto limits for purchasing + * @param params Limit parameters + * @param providerId Provider identifier + * @returns A promise that resolves to OnrampLimits + */ + getLimits(params: OnrampLimitParams, providerId?: string): Promise; + + /** + * Get the status of an ongoing or completed transaction + * @param params Transaction parameters including ID + * @param providerId Provider identifier + * @returns A promise that resolves to the transaction status + */ + getTransactionStatus(params: OnrampTransactionParams, providerId?: string): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params Onramp parameters (quote, user address, etc.) + * @param providerId Provider identifier (optional, uses default if not specified) + * @returns A promise that resolves to a URL string + */ + buildOnrampUrl(params: OnrampParams, providerId?: string): Promise; +} + +/** + * Interface that all onramp providers must implement + */ +export interface OnrampProviderInterface< + TQuoteOptions = unknown, + TOnrampOptions = unknown, + TLimitOptions = unknown, + TTransactionOptions = unknown, +> extends DefiProvider { + readonly type: 'onramp'; + + /** + * Unique identifier for the provider + */ + readonly providerId: string; + + /** + * Get a quote for onramping fiat to crypto + * @param params Quote parameters including provider-specific options + * @returns A promise that resolves to an OnrampQuote + */ + getQuote(params: OnrampQuoteParams): Promise; + + /** + * Get fiat/crypto limits for purchasing + * @param params Limit parameters + * @returns A promise that resolves to OnrampLimits + */ + getLimits(params: OnrampLimitParams): Promise; + + /** + * Get the status of an ongoing or completed transaction + * @param params Transaction parameters including ID + * @returns A promise that resolves to the transaction status + */ + getTransactionStatus(params: OnrampTransactionParams): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params Onramp parameters including provider-specific options + * @returns A promise that resolves to a URL string + */ + buildOnrampUrl(params: OnrampParams): Promise; +} diff --git a/packages/walletkit/src/api/interfaces/index.ts b/packages/walletkit/src/api/interfaces/index.ts index 49ebc0c94..d442b59be 100644 --- a/packages/walletkit/src/api/interfaces/index.ts +++ b/packages/walletkit/src/api/interfaces/index.ts @@ -12,6 +12,7 @@ export type { WalletSigner, ISigner } from './WalletSigner'; // Defi interfaces export type { DefiManagerAPI } from './DefiManagerAPI'; export type { SwapAPI, SwapProviderInterface } from './SwapAPI'; +export type { OnrampAPI, OnrampProviderInterface } from './OnrampAPI'; export type { DefiProvider } from './DefiProvider'; export type { TONConnectSessionManager } from './TONConnectSessionManager'; diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index 643b3b04a..60a705b2c 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -75,6 +75,18 @@ export type { SwapQuote } from './swaps/SwapQuote'; export type { SwapQuoteParams } from './swaps/SwapQuoteParams'; export type { SwapParams } from './swaps/SwapParams'; +// Onramp models +export type { + OnrampParams, + OnrampQuote, + OnrampQuoteParams, + OnrampLimits, + OnrampLimitParams, + OnrampTransactionStatus, + OnrampStatus, + OnrampTransactionParams, +} from './onramps'; + // Transaction models export * from './transactions/Transaction'; export type { TransactionAddressMetadata, TransactionAddressMetadataEntry } from './transactions/TransactionMetadata'; diff --git a/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts b/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts new file mode 100644 index 000000000..c5f564c94 --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface OnrampLimitParams { + /** + * Crypto currency ticker (e.g. 'ton') + */ + cryptoCurrency: string; + + /** + * Fiat currency ticker (e.g. 'usd') + */ + fiatCurrency: string; + + /** + * Provider-specific options (e.g., paymentMethod) + */ + providerOptions?: TProviderOptions; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampLimits.ts b/packages/walletkit/src/api/models/onramps/OnrampLimits.ts new file mode 100644 index 000000000..adecd6494 --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampLimits.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Onramp limits specify the boundaries of what a user can purchase + */ +export interface OnrampLimits { + /** + * Minimum fiat amount allowed + */ + minBaseAmount: number; + + /** + * Maximum fiat amount allowed + */ + maxBaseAmount: number; + + /** + * Minimum crypto amount allowed + */ + minQuoteAmount?: number; + + /** + * Maximum crypto amount allowed + */ + maxQuoteAmount?: number; + + /** + * Provider identifier + */ + providerId: string; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampParams.ts b/packages/walletkit/src/api/models/onramps/OnrampParams.ts new file mode 100644 index 000000000..9024cccde --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampParams.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress } from '../core/Primitives'; +import type { OnrampQuote } from './OnrampQuote'; + +/** + * Parameters for building an onramp URL + */ +export interface OnrampParams { + /** + * The onramp quote to base the transaction on (optional, some providers can generate URL without it) + */ + quote?: OnrampQuote; + + /** + * Address of the user receiving the crypto + */ + userAddress: UserFriendlyAddress; + + /** + * Provider-specific options + */ + providerOptions?: TProviderOptions; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampQuote.ts b/packages/walletkit/src/api/models/onramps/OnrampQuote.ts new file mode 100644 index 000000000..91e9b7245 --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampQuote.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Onramp quote response with pricing information + */ +export interface OnrampQuote { + /** + * Fiat currency ticker (e.g. 'USD') + */ + fiatCurrency: string; + + /** + * Crypto currency ticker (e.g. 'TON') + */ + cryptoCurrency: string; + + /** + * Amount of fiat to spend + */ + fiatAmount: string; + + /** + * Amount of crypto to receive + */ + cryptoAmount: string; + + /** + * Exchange rate (amount of crypto per 1 unit of fiat) + */ + rate: string; + + /** + * Total fees charged for the transaction (in fiat currency) + */ + fiatFee?: string; + + /** + * Network fee estimated (in fiat currency) + */ + networkFeeFiat?: string; + + /** + * Identifier of the onramp provider + */ + providerId: string; + + /** + * Provider-specific metadata for the quote + */ + metadata?: unknown; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampQuoteParams.ts b/packages/walletkit/src/api/models/onramps/OnrampQuoteParams.ts new file mode 100644 index 000000000..ef483a8de --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampQuoteParams.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Network } from '../core/Network'; + +/** + * Base parameters for requesting an onramp quote + */ +export interface OnrampQuoteParams { + /** + * Amount to onramp (either fiat or crypto, depending on isFiatAmount) + */ + amount: string; + + /** + * Fiat currency ticker (e.g. 'USD', 'EUR') + */ + fiatCurrency: string; + + /** + * Crypto currency ticker (e.g. 'TON', 'USDT') + */ + cryptoCurrency: string; + + /** + * Network on which the crypto will be received + */ + network: Network; + + /** + * Provider-specific options + */ + providerOptions?: TProviderOptions; + + /** + * If true, amount is the fiat amount to spend. If false, amount is the crypto amount to receive. + * Default depends on the provider implementation but usually defaults to true. + */ + isFiatAmount?: boolean; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts b/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts new file mode 100644 index 000000000..26c0b2b2e --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface OnrampTransactionParams { + /** + * The unique identifier assigned to the transaction by the provider + */ + transactionId: string; + + /** + * Provider-specific options + */ + providerOptions?: TProviderOptions; +} diff --git a/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts b/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts new file mode 100644 index 000000000..675c3831d --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export type OnrampStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'unknown'; + +/** + * Detailed status of an Onramp transaction + */ +export interface OnrampTransactionStatus { + /** + * Core status normalized mapping + */ + status: OnrampStatus; + + /** + * Provider's exact raw status string for reference + */ + rawStatus: string; + + /** + * Associated internal/provider transaction ID + */ + transactionId: string; + + /** + * Fiat currency used + */ + fiatCurrency: string; + + /** + * Fiat amount spent + */ + fiatAmount: string; + + /** + * Crypto currency bought + */ + cryptoCurrency: string; + + /** + * Blockchain transaction hash if available + */ + txHash?: string; + + /** + * Destination wallet address + */ + walletAddress?: string; + + /** + * Provider identifier + */ + providerId: string; +} diff --git a/packages/walletkit/src/api/models/onramps/index.ts b/packages/walletkit/src/api/models/onramps/index.ts new file mode 100644 index 000000000..7729c9fd5 --- /dev/null +++ b/packages/walletkit/src/api/models/onramps/index.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export type { OnrampParams } from './OnrampParams'; +export type { OnrampQuote } from './OnrampQuote'; +export type { OnrampQuoteParams } from './OnrampQuoteParams'; +export type { OnrampLimits } from './OnrampLimits'; +export type { OnrampLimitParams } from './OnrampLimitParams'; +export type { OnrampTransactionStatus, OnrampStatus } from './OnrampTransactionStatus'; +export type { OnrampTransactionParams } from './OnrampTransactionParams'; diff --git a/packages/walletkit/src/defi/index.ts b/packages/walletkit/src/defi/index.ts new file mode 100644 index 000000000..b8c982fc8 --- /dev/null +++ b/packages/walletkit/src/defi/index.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './errors'; +export * from './DefiManager'; +export * from './swap'; +export * from './onramp'; diff --git a/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts b/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts new file mode 100644 index 000000000..60bd202ab --- /dev/null +++ b/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { vi } from 'vitest'; + +import { OnrampManager } from './OnrampManager'; +import { OnrampProvider } from './OnrampProvider'; +import { OnrampError } from './errors'; + +class MockProvider extends OnrampProvider { + readonly providerId = 'mock_provider'; + + async getQuote() { + return { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + fiatAmount: '100', + cryptoAmount: '45', + rate: '0.45', + providerId: this.providerId, + }; + } + + async getLimits() { + return { + minBaseAmount: 10, + maxBaseAmount: 1000, + providerId: this.providerId, + }; + } + + async getTransactionStatus() { + return { + status: 'completed' as const, + rawStatus: 'completed', + transactionId: 'tx_123', + fiatCurrency: 'usd', + fiatAmount: '100', + cryptoCurrency: 'ton', + providerId: this.providerId, + }; + } + + async buildOnrampUrl() { + return 'https://mock.com/buy'; + } +} + +describe('OnrampManager', () => { + let manager: OnrampManager; + let mockProvider: MockProvider; + + beforeEach(() => { + manager = new OnrampManager(); + mockProvider = new MockProvider(); + }); + + describe('registration', () => { + it('should successfully register a provider', () => { + manager.registerProvider(mockProvider); + expect(manager.hasProvider('mock_provider')).toBe(true); + expect(manager.getRegisteredProviders()).toContain('mock_provider'); + }); + + it('should set the first registered provider as default', () => { + manager.registerProvider(mockProvider); + expect(manager.getProvider()).toBe(mockProvider); + }); + + it('should allow setting a default provider', () => { + const anotherMock = new MockProvider(); + // @ts-ignore + anotherMock.providerId = 'another_mock'; + + manager.registerProvider(mockProvider); + manager.registerProvider(anotherMock); + + manager.setDefaultProvider('another_mock'); + expect(manager.getProvider()).toBe(anotherMock); + }); + + it('should throw if setting unknown default provider', () => { + expect(() => manager.setDefaultProvider('unknown')).toThrow(OnrampError); + }); + }); + + describe('delegation', () => { + beforeEach(() => { + manager.registerProvider(mockProvider); + }); + + it('should delegate getQuote to the provider', async () => { + const spy = vi.spyOn(mockProvider, 'getQuote'); + const quote = await manager.getQuote({ + amount: '100', + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + network: 'mainnet', + }); + + expect(spy).toHaveBeenCalled(); + expect(quote.providerId).toBe('mock_provider'); + }); + + it('should delegate getLimits to the provider', async () => { + const spy = vi.spyOn(mockProvider, 'getLimits'); + const limits = await manager.getLimits({ + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + }); + + expect(spy).toHaveBeenCalled(); + expect(limits.providerId).toBe('mock_provider'); + }); + + it('should delegate getTransactionStatus to the provider', async () => { + const spy = vi.spyOn(mockProvider, 'getTransactionStatus'); + const status = await manager.getTransactionStatus({ + transactionId: '123', + }); + + expect(spy).toHaveBeenCalled(); + expect(status.providerId).toBe('mock_provider'); + }); + + it('should delegate buildOnrampUrl to the provider', async () => { + const spy = vi.spyOn(mockProvider, 'buildOnrampUrl'); + const url = await manager.buildOnrampUrl({ + userAddress: 'test_address', + }); + + expect(spy).toHaveBeenCalled(); + expect(url).toBe('https://mock.com/buy'); + }); + }); +}); diff --git a/packages/walletkit/src/defi/onramp/OnrampManager.ts b/packages/walletkit/src/defi/onramp/OnrampManager.ts new file mode 100644 index 000000000..454cab1f9 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/OnrampManager.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { OnrampAPI, OnrampProviderInterface } from '../../api/interfaces'; +import type { + OnrampParams, + OnrampQuote, + OnrampQuoteParams, + OnrampLimits, + OnrampLimitParams, + OnrampTransactionStatus, + OnrampTransactionParams, +} from '../../api/models/onramps'; +import { OnrampError } from './errors'; +import { globalLogger } from '../../core/Logger'; +import { DefiManager } from '../DefiManager'; + +const log = globalLogger.createChild('OnrampManager'); + +/** + * OnrampManager - manages onramp providers and delegates onramp operations + * + * Allows registration of multiple onramp providers and provides a unified API + * for fiat-to-crypto onramp operations. Providers can be switched dynamically. + */ +export class OnrampManager extends DefiManager implements OnrampAPI { + /** + * Get a quote for onramping fiat to crypto + * @param params - Quote parameters + * @param providerId - Optional provider name to use + * @returns Promise resolving to onramp quote + */ + async getQuote( + params: OnrampQuoteParams, + providerId?: string, + ): Promise { + const selectedProviderId = providerId || this.defaultProviderId; + log.debug('Getting onramp quote', { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + amount: params.amount, + isFiatAmount: params.isFiatAmount, + providerId: selectedProviderId, + }); + + try { + const quote = await this.getProvider(selectedProviderId).getQuote(params); + + log.debug('Received onramp quote', { + fiatAmount: quote.fiatAmount, + cryptoAmount: quote.cryptoAmount, + rate: quote.rate, + }); + + return quote; + } catch (error) { + log.error('Failed to get onramp quote', { error, params }); + throw error; + } + } + + /** + * Get fiat/crypto limits for purchasing + * @param params - Limit parameters + * @param providerId - Optional provider name to use + * @returns Promise resolving to onramp limits + */ + async getLimits( + params: OnrampLimitParams, + providerId?: string, + ): Promise { + const selectedProviderId = providerId || this.defaultProviderId; + log.debug('Getting onramp limits', { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + providerId: selectedProviderId, + }); + + try { + const limits = await this.getProvider(selectedProviderId).getLimits(params); + + log.debug('Received onramp limits', { + minBaseAmount: limits.minBaseAmount, + maxBaseAmount: limits.maxBaseAmount, + }); + + return limits; + } catch (error) { + log.error('Failed to get onramp limits', { error, params }); + throw error; + } + } + + /** + * Get the status of an ongoing or completed transaction + * @param params - Transaction parameters + * @param providerId - Optional provider name to use + * @returns Promise resolving to the transaction status + */ + async getTransactionStatus( + params: OnrampTransactionParams, + providerId?: string, + ): Promise { + const selectedProviderId = providerId || this.defaultProviderId; + log.debug('Getting onramp transaction status', { + transactionId: params.transactionId, + providerId: selectedProviderId, + }); + + try { + const status = await this.getProvider(selectedProviderId).getTransactionStatus(params); + + log.debug('Received onramp transaction status', { + status: status.status, + transactionId: status.transactionId, + }); + + return status; + } catch (error) { + log.error('Failed to get onramp transaction status', { error, params }); + throw error; + } + } + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params - Onramp parameters including quote + * @param providerId - Optional provider name to use + * @returns Promise resolving to a URL string + */ + async buildOnrampUrl( + params: OnrampParams, + providerId?: string, + ): Promise { + const selectedProviderId = providerId || params.quote?.providerId || this.defaultProviderId; + + log.debug('Building onramp URL', { + providerId: selectedProviderId, + userAddress: params.userAddress, + }); + + try { + const url = await this.getProvider(selectedProviderId).buildOnrampUrl(params); + + log.debug('Built onramp URL', { url: url.substring(0, 50) + '...' }); + + return url; + } catch (error) { + log.error('Failed to build onramp URL', { error, params }); + throw error; + } + } + + protected createError(message: string, code: string, details?: unknown): OnrampError { + return new OnrampError(message, code, details); + } +} diff --git a/packages/walletkit/src/defi/onramp/OnrampProvider.ts b/packages/walletkit/src/defi/onramp/OnrampProvider.ts new file mode 100644 index 000000000..e6f182010 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/OnrampProvider.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + OnrampParams, + OnrampQuote, + OnrampQuoteParams, + OnrampLimits, + OnrampLimitParams, + OnrampTransactionStatus, + OnrampTransactionParams, +} from '../../api/models/onramps'; +import type { OnrampProviderInterface } from '../../api/interfaces'; + +/** + * Abstract base class for onramp providers + * + * Provides a common interface for implementing fiat-to-crypto onramp functionality + * across different gateways. + * + * @example + * ```typescript + * class MyOnrampProvider extends OnrampProvider { + * async getQuote(params: OnrampQuoteParams): Promise { + * // Implementation + * } + * + * async buildOnrampUrl(params: OnrampParams): Promise { + * // Implementation + * } + * } + * ``` + */ +export abstract class OnrampProvider< + TQuoteOptions = undefined, + TOnrampOptions = undefined, + TLimitOptions = undefined, + TTransactionOptions = undefined, +> implements OnrampProviderInterface +{ + readonly type = 'onramp'; + abstract readonly providerId: string; + + /** + * Get a quote for onramping fiat to crypto + * @param params - Quote parameters including currencies and amount + * @returns Promise resolving to onramp quote with pricing information + */ + abstract getQuote(params: OnrampQuoteParams): Promise; + + /** + * Get trading limits for the provider + * @param params - Parameters specifying the desired currencies + * @returns Promise resolving to the allowed onramp limits + */ + abstract getLimits(params: OnrampLimitParams): Promise; + + /** + * Get the status of a specific onramp transaction + * @param params - Parameters including the transaction ID + * @returns Promise resolving to the current transaction status + */ + abstract getTransactionStatus( + params: OnrampTransactionParams, + ): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params - Onramp parameters including quote and user address + * @returns Promise resolving to a URL string + */ + abstract buildOnrampUrl(params: OnrampParams): Promise; +} diff --git a/packages/walletkit/src/defi/onramp/errors.ts b/packages/walletkit/src/defi/onramp/errors.ts new file mode 100644 index 000000000..dcad32e47 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/errors.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { DefiManagerError } from '../errors'; + +export class OnrampError extends DefiManagerError { + static readonly PROVIDER_ERROR = 'PROVIDER_ERROR'; + static readonly InvalidParams = 'INVALID_ONRAMP_PARAMS'; + static readonly QUOTE_FAILED = 'QUOTE_FAILED'; + static readonly URL_BUILD_FAILED = 'URL_BUILD_FAILED'; + + constructor(message: string, code: string, details?: unknown) { + super(message, code, details); + this.name = 'OnrampError'; + } +} diff --git a/packages/walletkit/src/defi/onramp/index.ts b/packages/walletkit/src/defi/onramp/index.ts new file mode 100644 index 000000000..1aa7fd462 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/index.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './errors'; +export * from './OnrampManager'; +export * from './OnrampProvider'; +export * from './moonpay'; diff --git a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts new file mode 100644 index 000000000..ebb60e9ad --- /dev/null +++ b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts @@ -0,0 +1,265 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { vi } from 'vitest'; + +import { MoonpayProvider } from './MoonpayProvider'; +import { OnrampError } from '../errors'; +import { Network } from '../../../api/models'; + +describe('MoonpayProvider', () => { + let provider: MoonpayProvider; + const apiKey = 'test_api_key'; + + beforeEach(() => { + provider = new MoonpayProvider(apiKey); + vi.spyOn(global, 'fetch'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should throw if apiKey is not provided', () => { + expect(() => new MoonpayProvider('')).toThrow(OnrampError); + }); + + it('should initialize successfully with an api key', () => { + const validProvider = new MoonpayProvider(apiKey); + expect(validProvider.providerId).toBe('moonpay'); + expect(validProvider.type).toBe('onramp'); + }); + }); + + describe('getQuote', () => { + it('should fetch and return a quote successfully', async () => { + const mockResponse = { + quoteCurrencyAmount: 45, + quoteCurrencyPrice: 0.45, + feeAmount: 2, + networkFeeAmount: 0.5, + }; + + (global.fetch as ReturnType).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const quote = await provider.getQuote({ + amount: '100', + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + network: 'mainnet', + }); + + expect(global.fetch).toHaveBeenCalledWith( + `https://api.moonpay.com/v3/currencies/ton/buy_quote?apiKey=${apiKey}&baseCurrencyCode=usd&baseCurrencyAmount=100`, + ); + + expect(quote).toEqual({ + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + fiatAmount: '100', + cryptoAmount: '45', + rate: '0.45', + fiatFee: '2', + networkFeeFiat: '0.5', + providerId: 'moonpay', + metadata: mockResponse, + }); + }); + + it('should throw an OnrampError if the fetch fails', async () => { + (global.fetch as ReturnType).mockResolvedValue({ + ok: false, + status: 500, + }); + + await expect( + provider.getQuote({ + amount: '100', + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + network: 'mainnet', + }), + ).rejects.toThrow(OnrampError); + }); + }); + + describe('getLimits', () => { + it('should fetch and return limits', async () => { + const mockResponse = { + baseCurrency: { minBuyAmount: 10, maxBuyAmount: 1000 }, + }; + + (global.fetch as ReturnType).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const limits = await provider.getLimits({ + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + }); + + expect(global.fetch).toHaveBeenCalledWith( + `https://api.moonpay.com/v3/currencies/ton/limits?apiKey=${apiKey}&baseCurrencyCode=usd`, + ); + + expect(limits).toEqual({ + minBaseAmount: 10, + maxBaseAmount: 1000, + providerId: 'moonpay', + }); + }); + + it('should throw if limits are empty or fetch fails', async () => { + (global.fetch as ReturnType).mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + await expect( + provider.getLimits({ + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + }), + ).rejects.toThrow(OnrampError); + }); + }); + + describe('getTransactionStatus', () => { + it('should normalized and return transaction status', async () => { + const mockResponse = { + status: 'completed', + id: 'tx_123', + baseCurrency: { code: 'usd' }, + baseCurrencyAmount: 100, + currency: { code: 'ton' }, + cryptoTransactionId: 'hash_456', + walletAddress: 'addr_789', + }; + + (global.fetch as ReturnType).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const status = await provider.getTransactionStatus({ transactionId: 'tx_123' }); + + expect(global.fetch).toHaveBeenCalledWith( + `https://api.moonpay.com/v1/transactions/tx_123?apiKey=${apiKey}`, + ); + + expect(status).toEqual({ + status: 'completed', + rawStatus: 'completed', + transactionId: 'tx_123', + fiatCurrency: 'usd', + fiatAmount: '100', + cryptoCurrency: 'ton', + txHash: 'hash_456', + walletAddress: 'addr_789', + providerId: 'moonpay', + }); + }); + }); + + describe('buildOnrampUrl', () => { + const userAddress = '0QCTestAddress...'; + + it('should build a basic url with default TON currency when no quote is provided', async () => { + const urlString = await provider.buildOnrampUrl({ + userAddress, + }); + + const url = new URL(urlString); + expect(url.origin).toBe('https://buy.moonpay.com'); + expect(url.searchParams.get('apiKey')).toBe(apiKey); + expect(url.searchParams.get('walletAddress')).toBe(userAddress); + expect(url.searchParams.get('currencyCode')).toBe('ton'); + }); + + it('should build a url using quote details if provided', async () => { + const urlString = await provider.buildOnrampUrl({ + userAddress, + quote: { + fiatCurrency: 'EUR', + cryptoCurrency: 'USDT', + fiatAmount: '500', + cryptoAmount: '530', + rate: '1.06', + providerId: 'moonpay', + }, + }); + + const url = new URL(urlString); + expect(url.searchParams.get('currencyCode')).toBe('usdt'); + expect(url.searchParams.get('baseCurrencyCode')).toBe('eur'); + expect(url.searchParams.get('baseCurrencyAmount')).toBe('500'); + }); + + it('should apply moonpay specific provider options', async () => { + const urlString = await provider.buildOnrampUrl({ + userAddress, + providerOptions: { + theme: 'dark', + redirectUrl: 'https://my-app.com/success', + }, + }); + + const url = new URL(urlString); + expect(url.searchParams.get('theme')).toBe('dark'); + expect(url.searchParams.get('redirectURL')).toBe('https://my-app.com/success'); + }); + }); +}); + +describe('MoonpayProvider Integration (Sandbox)', () => { + let provider: MoonpayProvider; + const sandboxApiKey = 'pk_test_J3c52pXIbsTmzwUtYJKQEpKwxuGw8me'; + + beforeEach(() => { + provider = new MoonpayProvider(sandboxApiKey); + }); + + it('should fetch real limits from Moonpay sandbox', async () => { + const limits = await provider.getLimits({ + fiatCurrency: 'usd', + cryptoCurrency: 'ton', + }); + + expect(limits).toBeDefined(); + expect(limits.minBaseAmount).toBeGreaterThan(0); + expect(limits.maxBaseAmount).toBeGreaterThan(0); + expect(limits.providerId).toBe('moonpay'); + }); + + it('should fetch a real quote from Moonpay sandbox', async () => { + const quote = await provider.getQuote({ + amount: '100', + fiatCurrency: 'usd', + cryptoCurrency: 'ton', + network: Network.mainnet(), + }); + + expect(quote).toBeDefined(); + expect(quote.fiatAmount).toBe('100'); + expect(quote.cryptoAmount).toBeDefined(); + expect(parseFloat(quote.cryptoAmount)).toBeGreaterThan(0); + expect(quote.rate).toBeDefined(); + expect(quote.providerId).toBe('moonpay'); + }); + + it('should throw when getting transaction status for an invalid ID from sandbox', async () => { + await expect(provider.getTransactionStatus({ transactionId: 'invalid_tx_id_123' })).rejects.toThrow( + OnrampError, + ); + }); +}); diff --git a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts new file mode 100644 index 000000000..cb7a512a8 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts @@ -0,0 +1,207 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + OnrampParams, + OnrampQuote, + OnrampQuoteParams, + OnrampLimits, + OnrampLimitParams, + OnrampTransactionStatus, + OnrampTransactionParams, + OnrampStatus, +} from '../../../api/models/onramps'; +import { OnrampProvider } from '../OnrampProvider'; +import { OnrampError } from '../errors'; + +/** + * Custom options for Moonpay requests + */ +export interface MoonpayQuoteOptions { + /** + * E.g. credit_card, google_pay, apple_pay. Limits the payment methods available. + */ + paymentMethod?: string; +} + +export interface MoonpayOnrampOptions { + /** + * E.g. dark or light color theme for the widget + */ + theme?: 'dark' | 'light'; + + /** + * The URL to redirect to after successful payment + */ + redirectUrl?: string; +} + +/** + * Provider implementation for Moonpay onramp + * + * Note: Moonpay relies heavily on widget redirects. Quotes are typically estimates + * and the final price is confirmed on the Moonpay widget. + */ +export class MoonpayProvider extends OnrampProvider { + readonly providerId = 'moonpay'; + + private readonly baseUrl = 'https://buy.moonpay.com'; + private readonly apiUrl = 'https://api.moonpay.com'; + private readonly apiKey: string; + + constructor(apiKey: string) { + super(); + if (!apiKey) { + throw new OnrampError('Moonpay API key is required', OnrampError.PROVIDER_ERROR); + } + this.apiKey = apiKey; + } + + /** + * Note: Moonpay's public API for quotes often requires server-side integration heavily. + * Often, wallets just use the URL generator and let Moonpay show the quote in the widget. + * We provide a mocked/base implementation here, you may need a server-to-server + * call to Moonpay's API to get an accurate quote without the widget. + */ + async getQuote(params: OnrampQuoteParams): Promise { + try { + const url = new URL(`${this.apiUrl}/v3/currencies/${params.cryptoCurrency.toLowerCase()}/buy_quote`); + url.searchParams.append('apiKey', this.apiKey); + url.searchParams.append('baseCurrencyCode', params.fiatCurrency.toLowerCase()); + url.searchParams.append('baseCurrencyAmount', params.amount); + + if (params.providerOptions?.paymentMethod) { + url.searchParams.append('paymentMethod', params.providerOptions.paymentMethod); + } + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + return { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + fiatAmount: params.amount, + cryptoAmount: data.quoteCurrencyAmount.toString(), + rate: data.quoteCurrencyPrice.toString(), + fiatFee: data.feeAmount.toString(), + networkFeeFiat: data.networkFeeAmount.toString(), + providerId: this.providerId, + metadata: data, + }; + } catch (error) { + throw new OnrampError('Failed to get Moonpay quote', OnrampError.QUOTE_FAILED, error); + } + } + + async getLimits(params: OnrampLimitParams): Promise { + try { + const url = new URL(`${this.apiUrl}/v3/currencies/${params.cryptoCurrency.toLowerCase()}/limits`); + url.searchParams.append('apiKey', this.apiKey); + url.searchParams.append('baseCurrencyCode', params.fiatCurrency.toLowerCase()); + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Moonpay limits format validation + if (!data.baseCurrency || typeof data.baseCurrency.minBuyAmount !== 'number') { + throw new Error('No limits returned from provider'); + } + + return { + minBaseAmount: data.baseCurrency.minBuyAmount, + maxBaseAmount: data.baseCurrency.maxBuyAmount, + providerId: this.providerId, + }; + } catch (error) { + throw new OnrampError('Failed to get Moonpay limits', OnrampError.PROVIDER_ERROR, error); + } + } + + async getTransactionStatus(params: OnrampTransactionParams): Promise { + try { + const url = new URL(`${this.apiUrl}/v1/transactions/${params.transactionId}`); + url.searchParams.append('apiKey', this.apiKey); + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + let normalizedStatus: OnrampStatus = 'unknown'; + switch (data.status) { + case 'pending': + case 'waitingPayment': + normalizedStatus = 'pending'; + break; + case 'completed': + normalizedStatus = 'completed'; + break; + case 'failed': + normalizedStatus = 'failed'; + break; + } + + return { + status: normalizedStatus, + rawStatus: data.status, + transactionId: data.id, + fiatCurrency: data.baseCurrency.code, + fiatAmount: data.baseCurrencyAmount.toString(), + cryptoCurrency: data.currency.code, + txHash: data.cryptoTransactionId, + walletAddress: data.walletAddress, + providerId: this.providerId, + }; + } catch (error) { + throw new OnrampError('Failed to get Moonpay transaction status', OnrampError.PROVIDER_ERROR, error); + } + } + + async buildOnrampUrl(params: OnrampParams): Promise { + try { + const url = new URL(this.baseUrl); + + url.searchParams.append('apiKey', this.apiKey); + url.searchParams.append('walletAddress', params.userAddress); + + // If we have a quote, we can prefill amounts and currencies + if (params.quote) { + // Moonpay expects lowercase currency codes + url.searchParams.append('currencyCode', params.quote.cryptoCurrency.toLowerCase()); + url.searchParams.append('baseCurrencyCode', params.quote.fiatCurrency.toLowerCase()); + url.searchParams.append('baseCurrencyAmount', params.quote.fiatAmount); + } else { + // Default to TON if no quote is provided + url.searchParams.append('currencyCode', 'ton'); + } + + // Apply specific provider options + if (params.providerOptions?.theme) { + url.searchParams.append('theme', params.providerOptions.theme); + } + + if (params.providerOptions?.redirectUrl) { + url.searchParams.append('redirectURL', params.providerOptions.redirectUrl); + } + + return url.toString(); + } catch (error) { + throw new OnrampError('Failed to build Moonpay URL', OnrampError.URL_BUILD_FAILED, error); + } + } +} diff --git a/packages/walletkit/src/defi/onramp/moonpay/index.ts b/packages/walletkit/src/defi/onramp/moonpay/index.ts new file mode 100644 index 000000000..cd3eb5473 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/moonpay/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './MoonpayProvider'; From 5d720d62cd52676224417701adb9e41aa254ebea Mon Sep 17 00:00:00 2001 From: "V. K." Date: Wed, 4 Mar 2026 09:52:37 +0400 Subject: [PATCH 02/67] feat(onramp): add mercuryo --- .../walletkit/src/api/interfaces/OnrampAPI.ts | 37 +-- packages/walletkit/src/api/models/index.ts | 5 - .../api/models/onramps/OnrampLimitParams.ts | 2 +- .../src/api/models/onramps/OnrampLimits.ts | 2 +- .../src/api/models/onramps/OnrampParams.ts | 9 +- .../models/onramps/OnrampTransactionParams.ts | 19 -- .../models/onramps/OnrampTransactionStatus.ts | 59 ---- .../walletkit/src/api/models/onramps/index.ts | 2 - packages/walletkit/src/defi/DefiManager.ts | 8 + .../src/defi/onramp/OnrampManager.spec.ts | 141 ---------- .../src/defi/onramp/OnrampManager.ts | 85 ++---- .../src/defi/onramp/OnrampProvider.ts | 34 +-- packages/walletkit/src/defi/onramp/index.ts | 1 + .../defi/onramp/mercuryo/MercuryoProvider.ts | 135 +++++++++ .../src/defi/onramp/mercuryo/index.ts | 9 + .../onramp/moonpay/MoonpayProvider.spec.ts | 265 ------------------ .../defi/onramp/moonpay/MoonpayProvider.ts | 106 +------ .../walletkit/src/defi/onramp/test-live.ts | 51 ++++ 18 files changed, 255 insertions(+), 715 deletions(-) delete mode 100644 packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts delete mode 100644 packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts delete mode 100644 packages/walletkit/src/defi/onramp/OnrampManager.spec.ts create mode 100644 packages/walletkit/src/defi/onramp/mercuryo/MercuryoProvider.ts create mode 100644 packages/walletkit/src/defi/onramp/mercuryo/index.ts delete mode 100644 packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts create mode 100644 packages/walletkit/src/defi/onramp/test-live.ts diff --git a/packages/walletkit/src/api/interfaces/OnrampAPI.ts b/packages/walletkit/src/api/interfaces/OnrampAPI.ts index 54ca9440c..134c9e0fa 100644 --- a/packages/walletkit/src/api/interfaces/OnrampAPI.ts +++ b/packages/walletkit/src/api/interfaces/OnrampAPI.ts @@ -10,10 +10,6 @@ import type { OnrampParams, OnrampQuote, OnrampQuoteParams, - OnrampLimits, - OnrampLimitParams, - OnrampTransactionStatus, - OnrampTransactionParams, } from '../models'; import type { DefiManagerAPI } from './DefiManagerAPI'; import type { DefiProvider } from './DefiProvider'; @@ -31,20 +27,11 @@ export interface OnrampAPI extends DefiManagerAPI { getQuote(params: OnrampQuoteParams, providerId?: string): Promise; /** - * Get fiat/crypto limits for purchasing - * @param params Limit parameters - * @param providerId Provider identifier - * @returns A promise that resolves to OnrampLimits - */ - getLimits(params: OnrampLimitParams, providerId?: string): Promise; - - /** - * Get the status of an ongoing or completed transaction - * @param params Transaction parameters including ID - * @param providerId Provider identifier - * @returns A promise that resolves to the transaction status + * Get quotes for onramping fiat to crypto from all registered providers + * @param params Quote parameters (fiat, crypto, amount, etc.) + * @returns A promise that resolves to an array of OnrampQuotes */ - getTransactionStatus(params: OnrampTransactionParams, providerId?: string): Promise; + getQuotes(params: OnrampQuoteParams): Promise; /** * Build an onramp URL for redirecting the user to the provider @@ -61,8 +48,6 @@ export interface OnrampAPI extends DefiManagerAPI { export interface OnrampProviderInterface< TQuoteOptions = unknown, TOnrampOptions = unknown, - TLimitOptions = unknown, - TTransactionOptions = unknown, > extends DefiProvider { readonly type: 'onramp'; @@ -78,20 +63,6 @@ export interface OnrampProviderInterface< */ getQuote(params: OnrampQuoteParams): Promise; - /** - * Get fiat/crypto limits for purchasing - * @param params Limit parameters - * @returns A promise that resolves to OnrampLimits - */ - getLimits(params: OnrampLimitParams): Promise; - - /** - * Get the status of an ongoing or completed transaction - * @param params Transaction parameters including ID - * @returns A promise that resolves to the transaction status - */ - getTransactionStatus(params: OnrampTransactionParams): Promise; - /** * Build an onramp URL for redirecting the user to the provider * @param params Onramp parameters including provider-specific options diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index 60a705b2c..ee22bd417 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -80,11 +80,6 @@ export type { OnrampParams, OnrampQuote, OnrampQuoteParams, - OnrampLimits, - OnrampLimitParams, - OnrampTransactionStatus, - OnrampStatus, - OnrampTransactionParams, } from './onramps'; // Transaction models diff --git a/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts b/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts index c5f564c94..d2df9b2ba 100644 --- a/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts +++ b/packages/walletkit/src/api/models/onramps/OnrampLimitParams.ts @@ -11,7 +11,7 @@ export interface OnrampLimitParams { * Crypto currency ticker (e.g. 'ton') */ cryptoCurrency: string; - + /** * Fiat currency ticker (e.g. 'usd') */ diff --git a/packages/walletkit/src/api/models/onramps/OnrampLimits.ts b/packages/walletkit/src/api/models/onramps/OnrampLimits.ts index adecd6494..7a8f58dc6 100644 --- a/packages/walletkit/src/api/models/onramps/OnrampLimits.ts +++ b/packages/walletkit/src/api/models/onramps/OnrampLimits.ts @@ -29,7 +29,7 @@ export interface OnrampLimits { * Maximum crypto amount allowed */ maxQuoteAmount?: number; - + /** * Provider identifier */ diff --git a/packages/walletkit/src/api/models/onramps/OnrampParams.ts b/packages/walletkit/src/api/models/onramps/OnrampParams.ts index 9024cccde..f676f1d11 100644 --- a/packages/walletkit/src/api/models/onramps/OnrampParams.ts +++ b/packages/walletkit/src/api/models/onramps/OnrampParams.ts @@ -14,15 +14,20 @@ import type { OnrampQuote } from './OnrampQuote'; */ export interface OnrampParams { /** - * The onramp quote to base the transaction on (optional, some providers can generate URL without it) + * The onramp quote to base the transaction on */ - quote?: OnrampQuote; + quote: OnrampQuote; /** * Address of the user receiving the crypto */ userAddress: UserFriendlyAddress; + /** + * URL to redirect the user to after a successful transaction (if supported by provider) + */ + redirectUrl?: string; + /** * Provider-specific options */ diff --git a/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts b/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts deleted file mode 100644 index 26c0b2b2e..000000000 --- a/packages/walletkit/src/api/models/onramps/OnrampTransactionParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export interface OnrampTransactionParams { - /** - * The unique identifier assigned to the transaction by the provider - */ - transactionId: string; - - /** - * Provider-specific options - */ - providerOptions?: TProviderOptions; -} diff --git a/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts b/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts deleted file mode 100644 index 675c3831d..000000000 --- a/packages/walletkit/src/api/models/onramps/OnrampTransactionStatus.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export type OnrampStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'unknown'; - -/** - * Detailed status of an Onramp transaction - */ -export interface OnrampTransactionStatus { - /** - * Core status normalized mapping - */ - status: OnrampStatus; - - /** - * Provider's exact raw status string for reference - */ - rawStatus: string; - - /** - * Associated internal/provider transaction ID - */ - transactionId: string; - - /** - * Fiat currency used - */ - fiatCurrency: string; - - /** - * Fiat amount spent - */ - fiatAmount: string; - - /** - * Crypto currency bought - */ - cryptoCurrency: string; - - /** - * Blockchain transaction hash if available - */ - txHash?: string; - - /** - * Destination wallet address - */ - walletAddress?: string; - - /** - * Provider identifier - */ - providerId: string; -} diff --git a/packages/walletkit/src/api/models/onramps/index.ts b/packages/walletkit/src/api/models/onramps/index.ts index 7729c9fd5..08bbda260 100644 --- a/packages/walletkit/src/api/models/onramps/index.ts +++ b/packages/walletkit/src/api/models/onramps/index.ts @@ -11,5 +11,3 @@ export type { OnrampQuote } from './OnrampQuote'; export type { OnrampQuoteParams } from './OnrampQuoteParams'; export type { OnrampLimits } from './OnrampLimits'; export type { OnrampLimitParams } from './OnrampLimitParams'; -export type { OnrampTransactionStatus, OnrampStatus } from './OnrampTransactionStatus'; -export type { OnrampTransactionParams } from './OnrampTransactionParams'; diff --git a/packages/walletkit/src/defi/DefiManager.ts b/packages/walletkit/src/defi/DefiManager.ts index 8a5a930a0..72efc3ec9 100644 --- a/packages/walletkit/src/defi/DefiManager.ts +++ b/packages/walletkit/src/defi/DefiManager.ts @@ -90,6 +90,14 @@ export abstract class DefiManager implements DefiManager return Array.from(this.providers.keys()); } + /** + * Get list of registered provider instances + * @returns Array of provider instances + */ + getProviders(): T[] { + return Array.from(this.providers.values()); + } + /** * Check if a provider is registered * @param providerId - Provider id diff --git a/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts b/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts deleted file mode 100644 index 60bd202ab..000000000 --- a/packages/walletkit/src/defi/onramp/OnrampManager.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { vi } from 'vitest'; - -import { OnrampManager } from './OnrampManager'; -import { OnrampProvider } from './OnrampProvider'; -import { OnrampError } from './errors'; - -class MockProvider extends OnrampProvider { - readonly providerId = 'mock_provider'; - - async getQuote() { - return { - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - fiatAmount: '100', - cryptoAmount: '45', - rate: '0.45', - providerId: this.providerId, - }; - } - - async getLimits() { - return { - minBaseAmount: 10, - maxBaseAmount: 1000, - providerId: this.providerId, - }; - } - - async getTransactionStatus() { - return { - status: 'completed' as const, - rawStatus: 'completed', - transactionId: 'tx_123', - fiatCurrency: 'usd', - fiatAmount: '100', - cryptoCurrency: 'ton', - providerId: this.providerId, - }; - } - - async buildOnrampUrl() { - return 'https://mock.com/buy'; - } -} - -describe('OnrampManager', () => { - let manager: OnrampManager; - let mockProvider: MockProvider; - - beforeEach(() => { - manager = new OnrampManager(); - mockProvider = new MockProvider(); - }); - - describe('registration', () => { - it('should successfully register a provider', () => { - manager.registerProvider(mockProvider); - expect(manager.hasProvider('mock_provider')).toBe(true); - expect(manager.getRegisteredProviders()).toContain('mock_provider'); - }); - - it('should set the first registered provider as default', () => { - manager.registerProvider(mockProvider); - expect(manager.getProvider()).toBe(mockProvider); - }); - - it('should allow setting a default provider', () => { - const anotherMock = new MockProvider(); - // @ts-ignore - anotherMock.providerId = 'another_mock'; - - manager.registerProvider(mockProvider); - manager.registerProvider(anotherMock); - - manager.setDefaultProvider('another_mock'); - expect(manager.getProvider()).toBe(anotherMock); - }); - - it('should throw if setting unknown default provider', () => { - expect(() => manager.setDefaultProvider('unknown')).toThrow(OnrampError); - }); - }); - - describe('delegation', () => { - beforeEach(() => { - manager.registerProvider(mockProvider); - }); - - it('should delegate getQuote to the provider', async () => { - const spy = vi.spyOn(mockProvider, 'getQuote'); - const quote = await manager.getQuote({ - amount: '100', - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - network: 'mainnet', - }); - - expect(spy).toHaveBeenCalled(); - expect(quote.providerId).toBe('mock_provider'); - }); - - it('should delegate getLimits to the provider', async () => { - const spy = vi.spyOn(mockProvider, 'getLimits'); - const limits = await manager.getLimits({ - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - }); - - expect(spy).toHaveBeenCalled(); - expect(limits.providerId).toBe('mock_provider'); - }); - - it('should delegate getTransactionStatus to the provider', async () => { - const spy = vi.spyOn(mockProvider, 'getTransactionStatus'); - const status = await manager.getTransactionStatus({ - transactionId: '123', - }); - - expect(spy).toHaveBeenCalled(); - expect(status.providerId).toBe('mock_provider'); - }); - - it('should delegate buildOnrampUrl to the provider', async () => { - const spy = vi.spyOn(mockProvider, 'buildOnrampUrl'); - const url = await manager.buildOnrampUrl({ - userAddress: 'test_address', - }); - - expect(spy).toHaveBeenCalled(); - expect(url).toBe('https://mock.com/buy'); - }); - }); -}); diff --git a/packages/walletkit/src/defi/onramp/OnrampManager.ts b/packages/walletkit/src/defi/onramp/OnrampManager.ts index 454cab1f9..26716a5a1 100644 --- a/packages/walletkit/src/defi/onramp/OnrampManager.ts +++ b/packages/walletkit/src/defi/onramp/OnrampManager.ts @@ -7,15 +7,7 @@ */ import type { OnrampAPI, OnrampProviderInterface } from '../../api/interfaces'; -import type { - OnrampParams, - OnrampQuote, - OnrampQuoteParams, - OnrampLimits, - OnrampLimitParams, - OnrampTransactionStatus, - OnrampTransactionParams, -} from '../../api/models/onramps'; +import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../api/models/onramps'; import { OnrampError } from './errors'; import { globalLogger } from '../../core/Logger'; import { DefiManager } from '../DefiManager'; @@ -65,66 +57,43 @@ export class OnrampManager extends DefiManager implemen } /** - * Get fiat/crypto limits for purchasing - * @param params - Limit parameters - * @param providerId - Optional provider name to use - * @returns Promise resolving to onramp limits + * Get quotes for onramping fiat to crypto from all registered providers + * @param params - Quote parameters + * @returns Promise resolving to an array of onramp quotes */ - async getLimits( - params: OnrampLimitParams, - providerId?: string, - ): Promise { - const selectedProviderId = providerId || this.defaultProviderId; - log.debug('Getting onramp limits', { + async getQuotes(params: OnrampQuoteParams): Promise { + log.debug('Getting onramp quotes from all providers', { fiatCurrency: params.fiatCurrency, cryptoCurrency: params.cryptoCurrency, - providerId: selectedProviderId, + amount: params.amount, + isFiatAmount: params.isFiatAmount, }); - try { - const limits = await this.getProvider(selectedProviderId).getLimits(params); + const providers = this.getProviders(); - log.debug('Received onramp limits', { - minBaseAmount: limits.minBaseAmount, - maxBaseAmount: limits.maxBaseAmount, - }); + // Execute all getQuote requests concurrently + const results = await Promise.allSettled( + providers.map((provider: OnrampProviderInterface) => provider.getQuote(params)), + ); - return limits; - } catch (error) { - log.error('Failed to get onramp limits', { error, params }); - throw error; - } - } + const quotes: OnrampQuote[] = []; - /** - * Get the status of an ongoing or completed transaction - * @param params - Transaction parameters - * @param providerId - Optional provider name to use - * @returns Promise resolving to the transaction status - */ - async getTransactionStatus( - params: OnrampTransactionParams, - providerId?: string, - ): Promise { - const selectedProviderId = providerId || this.defaultProviderId; - log.debug('Getting onramp transaction status', { - transactionId: params.transactionId, - providerId: selectedProviderId, + results.forEach((result: PromiseSettledResult, index: number) => { + if (result.status === 'fulfilled') { + quotes.push(result.value); + } else { + log.debug(`Provider ${providers[index].providerId} failed to return a quote`, { + error: result.reason, + params, + }); + } }); - try { - const status = await this.getProvider(selectedProviderId).getTransactionStatus(params); - - log.debug('Received onramp transaction status', { - status: status.status, - transactionId: status.transactionId, - }); + log.debug(`Received ${quotes.length} onramp quotes`, { + successfulProviders: quotes.map((q) => q.providerId), + }); - return status; - } catch (error) { - log.error('Failed to get onramp transaction status', { error, params }); - throw error; - } + return quotes; } /** diff --git a/packages/walletkit/src/defi/onramp/OnrampProvider.ts b/packages/walletkit/src/defi/onramp/OnrampProvider.ts index e6f182010..500d73a87 100644 --- a/packages/walletkit/src/defi/onramp/OnrampProvider.ts +++ b/packages/walletkit/src/defi/onramp/OnrampProvider.ts @@ -6,15 +6,7 @@ * */ -import type { - OnrampParams, - OnrampQuote, - OnrampQuoteParams, - OnrampLimits, - OnrampLimitParams, - OnrampTransactionStatus, - OnrampTransactionParams, -} from '../../api/models/onramps'; +import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../api/models/onramps'; import type { OnrampProviderInterface } from '../../api/interfaces'; /** @@ -36,12 +28,8 @@ import type { OnrampProviderInterface } from '../../api/interfaces'; * } * ``` */ -export abstract class OnrampProvider< - TQuoteOptions = undefined, - TOnrampOptions = undefined, - TLimitOptions = undefined, - TTransactionOptions = undefined, -> implements OnrampProviderInterface +export abstract class OnrampProvider + implements OnrampProviderInterface { readonly type = 'onramp'; abstract readonly providerId: string; @@ -53,22 +41,6 @@ export abstract class OnrampProvider< */ abstract getQuote(params: OnrampQuoteParams): Promise; - /** - * Get trading limits for the provider - * @param params - Parameters specifying the desired currencies - * @returns Promise resolving to the allowed onramp limits - */ - abstract getLimits(params: OnrampLimitParams): Promise; - - /** - * Get the status of a specific onramp transaction - * @param params - Parameters including the transaction ID - * @returns Promise resolving to the current transaction status - */ - abstract getTransactionStatus( - params: OnrampTransactionParams, - ): Promise; - /** * Build an onramp URL for redirecting the user to the provider * @param params - Onramp parameters including quote and user address diff --git a/packages/walletkit/src/defi/onramp/index.ts b/packages/walletkit/src/defi/onramp/index.ts index 1aa7fd462..23e8f2bc3 100644 --- a/packages/walletkit/src/defi/onramp/index.ts +++ b/packages/walletkit/src/defi/onramp/index.ts @@ -10,3 +10,4 @@ export * from './errors'; export * from './OnrampManager'; export * from './OnrampProvider'; export * from './moonpay'; +export * from './mercuryo'; diff --git a/packages/walletkit/src/defi/onramp/mercuryo/MercuryoProvider.ts b/packages/walletkit/src/defi/onramp/mercuryo/MercuryoProvider.ts new file mode 100644 index 000000000..13f082cec --- /dev/null +++ b/packages/walletkit/src/defi/onramp/mercuryo/MercuryoProvider.ts @@ -0,0 +1,135 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models/onramps'; +import { OnrampProvider } from '../OnrampProvider'; +import { OnrampError } from '../errors'; + +/** + * Custom options for Mercuryo requests + */ +export interface MercuryoQuoteOptions { + /** + * E.g. credit_card, bank_transfer. Limits the payment methods available. + * Often not strictly required by convert endpoint but can be passed. + */ + paymentMethod?: string; +} + +export interface MercuryoOnrampOptions { + /** + * E.g. The exact widget ID assigned by Mercuryo to the partner + */ + widgetId?: string; + + /** + * E.g. The user's email to pre-fill the widget + */ + merchantUserEmail?: string; +} + +/** + * Provider implementation for Mercuryo onramp + */ +export class MercuryoProvider extends OnrampProvider { + readonly providerId = 'mercuryo'; + + private readonly baseUrl = 'https://exchange.mercuryo.io/'; + private readonly apiUrl = 'https://api.mercuryo.io/v1.6'; + + /** + * Optional default widget ID + */ + private readonly widgetId?: string; + + constructor(widgetId?: string) { + super(); + this.widgetId = widgetId; + } + + async getQuote(params: OnrampQuoteParams): Promise { + try { + // Amount string validation + const amount = parseFloat(params.amount); + if (isNaN(amount) || amount <= 0) { + throw new Error('Invalid amount'); + } + + const url = new URL(`${this.apiUrl}/widget/buy/rate`); + + url.searchParams.append('fiat_currency', params.fiatCurrency.toUpperCase()); + url.searchParams.append('currency', params.cryptoCurrency.toUpperCase()); + url.searchParams.append('amount', params.amount); + + if (this.widgetId) { + url.searchParams.append('widget_id', this.widgetId); + } + + if (params.providerOptions?.paymentMethod) { + url.searchParams.append('payment_method', params.providerOptions.paymentMethod); + } + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!data.data) { + throw new Error('No quote data returned'); + } + + return { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + fiatAmount: params.amount, + cryptoAmount: data.data.amount.toString(), + rate: data.data.rate.toString(), + fiatFee: data.data.fee ? data.data.fee.toString() : '0', + providerId: this.providerId, + metadata: data.data, + }; + } catch (error) { + throw new OnrampError('Failed to get Mercuryo quote', OnrampError.QUOTE_FAILED, error); + } + } + + async buildOnrampUrl(params: OnrampParams): Promise { + try { + const url = new URL(this.baseUrl); + const activeWidgetId = params.providerOptions?.widgetId || this.widgetId; + + if (activeWidgetId) { + url.searchParams.append('widget_id', activeWidgetId); + } + + // Prefill with user address + if (params.userAddress) { + url.searchParams.append('address', params.userAddress); + } + + if (params.providerOptions?.merchantUserEmail) { + url.searchParams.append('merchant_user_email', params.providerOptions.merchantUserEmail); + } + + // Apply quote details + url.searchParams.append('fiat_currency', params.quote.fiatCurrency.toUpperCase()); + url.searchParams.append('fiat_amount', params.quote.fiatAmount); + url.searchParams.append('currency', params.quote.cryptoCurrency.toUpperCase()); + + if (params.redirectUrl) { + url.searchParams.append('return_url', params.redirectUrl); + } + + return url.toString(); + } catch (error) { + throw new OnrampError('Failed to build Mercuryo URL', OnrampError.URL_BUILD_FAILED, error); + } + } +} diff --git a/packages/walletkit/src/defi/onramp/mercuryo/index.ts b/packages/walletkit/src/defi/onramp/mercuryo/index.ts new file mode 100644 index 000000000..7e63c6a97 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/mercuryo/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './MercuryoProvider'; diff --git a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts deleted file mode 100644 index ebb60e9ad..000000000 --- a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { vi } from 'vitest'; - -import { MoonpayProvider } from './MoonpayProvider'; -import { OnrampError } from '../errors'; -import { Network } from '../../../api/models'; - -describe('MoonpayProvider', () => { - let provider: MoonpayProvider; - const apiKey = 'test_api_key'; - - beforeEach(() => { - provider = new MoonpayProvider(apiKey); - vi.spyOn(global, 'fetch'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('constructor', () => { - it('should throw if apiKey is not provided', () => { - expect(() => new MoonpayProvider('')).toThrow(OnrampError); - }); - - it('should initialize successfully with an api key', () => { - const validProvider = new MoonpayProvider(apiKey); - expect(validProvider.providerId).toBe('moonpay'); - expect(validProvider.type).toBe('onramp'); - }); - }); - - describe('getQuote', () => { - it('should fetch and return a quote successfully', async () => { - const mockResponse = { - quoteCurrencyAmount: 45, - quoteCurrencyPrice: 0.45, - feeAmount: 2, - networkFeeAmount: 0.5, - }; - - (global.fetch as ReturnType).mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }); - - const quote = await provider.getQuote({ - amount: '100', - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - network: 'mainnet', - }); - - expect(global.fetch).toHaveBeenCalledWith( - `https://api.moonpay.com/v3/currencies/ton/buy_quote?apiKey=${apiKey}&baseCurrencyCode=usd&baseCurrencyAmount=100`, - ); - - expect(quote).toEqual({ - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - fiatAmount: '100', - cryptoAmount: '45', - rate: '0.45', - fiatFee: '2', - networkFeeFiat: '0.5', - providerId: 'moonpay', - metadata: mockResponse, - }); - }); - - it('should throw an OnrampError if the fetch fails', async () => { - (global.fetch as ReturnType).mockResolvedValue({ - ok: false, - status: 500, - }); - - await expect( - provider.getQuote({ - amount: '100', - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - network: 'mainnet', - }), - ).rejects.toThrow(OnrampError); - }); - }); - - describe('getLimits', () => { - it('should fetch and return limits', async () => { - const mockResponse = { - baseCurrency: { minBuyAmount: 10, maxBuyAmount: 1000 }, - }; - - (global.fetch as ReturnType).mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }); - - const limits = await provider.getLimits({ - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - }); - - expect(global.fetch).toHaveBeenCalledWith( - `https://api.moonpay.com/v3/currencies/ton/limits?apiKey=${apiKey}&baseCurrencyCode=usd`, - ); - - expect(limits).toEqual({ - minBaseAmount: 10, - maxBaseAmount: 1000, - providerId: 'moonpay', - }); - }); - - it('should throw if limits are empty or fetch fails', async () => { - (global.fetch as ReturnType).mockResolvedValue({ - ok: true, - json: async () => ({}), - }); - - await expect( - provider.getLimits({ - fiatCurrency: 'USD', - cryptoCurrency: 'TON', - }), - ).rejects.toThrow(OnrampError); - }); - }); - - describe('getTransactionStatus', () => { - it('should normalized and return transaction status', async () => { - const mockResponse = { - status: 'completed', - id: 'tx_123', - baseCurrency: { code: 'usd' }, - baseCurrencyAmount: 100, - currency: { code: 'ton' }, - cryptoTransactionId: 'hash_456', - walletAddress: 'addr_789', - }; - - (global.fetch as ReturnType).mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }); - - const status = await provider.getTransactionStatus({ transactionId: 'tx_123' }); - - expect(global.fetch).toHaveBeenCalledWith( - `https://api.moonpay.com/v1/transactions/tx_123?apiKey=${apiKey}`, - ); - - expect(status).toEqual({ - status: 'completed', - rawStatus: 'completed', - transactionId: 'tx_123', - fiatCurrency: 'usd', - fiatAmount: '100', - cryptoCurrency: 'ton', - txHash: 'hash_456', - walletAddress: 'addr_789', - providerId: 'moonpay', - }); - }); - }); - - describe('buildOnrampUrl', () => { - const userAddress = '0QCTestAddress...'; - - it('should build a basic url with default TON currency when no quote is provided', async () => { - const urlString = await provider.buildOnrampUrl({ - userAddress, - }); - - const url = new URL(urlString); - expect(url.origin).toBe('https://buy.moonpay.com'); - expect(url.searchParams.get('apiKey')).toBe(apiKey); - expect(url.searchParams.get('walletAddress')).toBe(userAddress); - expect(url.searchParams.get('currencyCode')).toBe('ton'); - }); - - it('should build a url using quote details if provided', async () => { - const urlString = await provider.buildOnrampUrl({ - userAddress, - quote: { - fiatCurrency: 'EUR', - cryptoCurrency: 'USDT', - fiatAmount: '500', - cryptoAmount: '530', - rate: '1.06', - providerId: 'moonpay', - }, - }); - - const url = new URL(urlString); - expect(url.searchParams.get('currencyCode')).toBe('usdt'); - expect(url.searchParams.get('baseCurrencyCode')).toBe('eur'); - expect(url.searchParams.get('baseCurrencyAmount')).toBe('500'); - }); - - it('should apply moonpay specific provider options', async () => { - const urlString = await provider.buildOnrampUrl({ - userAddress, - providerOptions: { - theme: 'dark', - redirectUrl: 'https://my-app.com/success', - }, - }); - - const url = new URL(urlString); - expect(url.searchParams.get('theme')).toBe('dark'); - expect(url.searchParams.get('redirectURL')).toBe('https://my-app.com/success'); - }); - }); -}); - -describe('MoonpayProvider Integration (Sandbox)', () => { - let provider: MoonpayProvider; - const sandboxApiKey = 'pk_test_J3c52pXIbsTmzwUtYJKQEpKwxuGw8me'; - - beforeEach(() => { - provider = new MoonpayProvider(sandboxApiKey); - }); - - it('should fetch real limits from Moonpay sandbox', async () => { - const limits = await provider.getLimits({ - fiatCurrency: 'usd', - cryptoCurrency: 'ton', - }); - - expect(limits).toBeDefined(); - expect(limits.minBaseAmount).toBeGreaterThan(0); - expect(limits.maxBaseAmount).toBeGreaterThan(0); - expect(limits.providerId).toBe('moonpay'); - }); - - it('should fetch a real quote from Moonpay sandbox', async () => { - const quote = await provider.getQuote({ - amount: '100', - fiatCurrency: 'usd', - cryptoCurrency: 'ton', - network: Network.mainnet(), - }); - - expect(quote).toBeDefined(); - expect(quote.fiatAmount).toBe('100'); - expect(quote.cryptoAmount).toBeDefined(); - expect(parseFloat(quote.cryptoAmount)).toBeGreaterThan(0); - expect(quote.rate).toBeDefined(); - expect(quote.providerId).toBe('moonpay'); - }); - - it('should throw when getting transaction status for an invalid ID from sandbox', async () => { - await expect(provider.getTransactionStatus({ transactionId: 'invalid_tx_id_123' })).rejects.toThrow( - OnrampError, - ); - }); -}); diff --git a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts index cb7a512a8..65199460d 100644 --- a/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts +++ b/packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts @@ -6,16 +6,7 @@ * */ -import type { - OnrampParams, - OnrampQuote, - OnrampQuoteParams, - OnrampLimits, - OnrampLimitParams, - OnrampTransactionStatus, - OnrampTransactionParams, - OnrampStatus, -} from '../../../api/models/onramps'; +import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models/onramps'; import { OnrampProvider } from '../OnrampProvider'; import { OnrampError } from '../errors'; @@ -34,11 +25,6 @@ export interface MoonpayOnrampOptions { * E.g. dark or light color theme for the widget */ theme?: 'dark' | 'light'; - - /** - * The URL to redirect to after successful payment - */ - redirectUrl?: string; } /** @@ -47,7 +33,7 @@ export interface MoonpayOnrampOptions { * Note: Moonpay relies heavily on widget redirects. Quotes are typically estimates * and the final price is confirmed on the Moonpay widget. */ -export class MoonpayProvider extends OnrampProvider { +export class MoonpayProvider extends OnrampProvider { readonly providerId = 'moonpay'; private readonly baseUrl = 'https://buy.moonpay.com'; @@ -102,76 +88,6 @@ export class MoonpayProvider extends OnrampProvider): Promise { - try { - const url = new URL(`${this.apiUrl}/v3/currencies/${params.cryptoCurrency.toLowerCase()}/limits`); - url.searchParams.append('apiKey', this.apiKey); - url.searchParams.append('baseCurrencyCode', params.fiatCurrency.toLowerCase()); - - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - // Moonpay limits format validation - if (!data.baseCurrency || typeof data.baseCurrency.minBuyAmount !== 'number') { - throw new Error('No limits returned from provider'); - } - - return { - minBaseAmount: data.baseCurrency.minBuyAmount, - maxBaseAmount: data.baseCurrency.maxBuyAmount, - providerId: this.providerId, - }; - } catch (error) { - throw new OnrampError('Failed to get Moonpay limits', OnrampError.PROVIDER_ERROR, error); - } - } - - async getTransactionStatus(params: OnrampTransactionParams): Promise { - try { - const url = new URL(`${this.apiUrl}/v1/transactions/${params.transactionId}`); - url.searchParams.append('apiKey', this.apiKey); - - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - let normalizedStatus: OnrampStatus = 'unknown'; - switch (data.status) { - case 'pending': - case 'waitingPayment': - normalizedStatus = 'pending'; - break; - case 'completed': - normalizedStatus = 'completed'; - break; - case 'failed': - normalizedStatus = 'failed'; - break; - } - - return { - status: normalizedStatus, - rawStatus: data.status, - transactionId: data.id, - fiatCurrency: data.baseCurrency.code, - fiatAmount: data.baseCurrencyAmount.toString(), - cryptoCurrency: data.currency.code, - txHash: data.cryptoTransactionId, - walletAddress: data.walletAddress, - providerId: this.providerId, - }; - } catch (error) { - throw new OnrampError('Failed to get Moonpay transaction status', OnrampError.PROVIDER_ERROR, error); - } - } - async buildOnrampUrl(params: OnrampParams): Promise { try { const url = new URL(this.baseUrl); @@ -179,24 +95,18 @@ export class MoonpayProvider extends OnrampProvider Date: Tue, 7 Apr 2026 10:44:37 +0400 Subject: [PATCH 03/67] feat(onramp): start onramp widget --- .../token-list-sheet/token-list-sheet.tsx | 6 +- .../currency-item/currency-item.module.css | 19 +- .../currency-item/currency-item.stories.tsx | 13 +- .../currency-item/currency-item.tsx | 137 ++++++++++++ .../components/currency-item/index.ts | 0 .../currency-select-modal.module.css} | 0 .../currency-select-modal.tsx | 77 +++++++ .../components/currency-select-modal/index.ts | 10 + .../token-select-modal/token-select-modal.tsx | 55 ++--- .../components/token-selector/index.ts | 0 .../token-selector/token-selector.module.css | 6 +- .../token-selector/token-selector.tsx | 21 +- .../currency-item/currency-item.tsx | 63 ------ .../src/features/balances/index.ts | 1 - .../components/onramp-amount-input/index.ts | 10 + .../onramp-amount-input.module.css | 57 +++++ .../onramp-amount-input.tsx | 61 ++++++ .../components/onramp-amount-presets/index.ts | 10 + .../onramp-amount-presets.module.css | 25 +++ .../onramp-amount-presets.tsx | 37 ++++ .../components/onramp-checkout/index.ts | 10 + .../onramp-checkout.module.css | 41 ++++ .../onramp-checkout/onramp-checkout.tsx | 65 ++++++ .../components/onramp-currency-item/index.ts | 10 + .../onramp-currency-item.tsx | 31 +++ .../onramp-currency-select-modal/index.ts | 10 + .../onramp-currency-select-modal.tsx | 60 ++++++ .../onramp-provider-select/index.ts | 10 + .../onramp-provider-select.module.css | 54 +++++ .../onramp-provider-select.tsx | 47 ++++ .../onramp-token-select-modal/index.ts | 10 + .../onramp-token-select-modal.tsx | 18 ++ .../onramp-token-selectors/index.ts | 10 + .../onramp-token-selectors.module.css | 11 + .../onramp-token-selectors.tsx | 49 +++++ .../onramp-widget-provider/index.ts | 10 + .../onramp-widget-provider.tsx | 203 ++++++++++++++++++ .../components/onramp-widget-ui/index.ts | 10 + .../onramp-widget-ui.module.css | 23 ++ .../onramp-widget-ui/onramp-widget-ui.tsx | 144 +++++++++++++ .../onramp/components/onramp-widget/index.ts | 10 + .../onramp-widget/onramp-widget.stories.tsx | 67 ++++++ .../onramp-widget/onramp-widget.tsx | 41 ++++ .../appkit-react/src/features/onramp/index.ts | 11 + .../features/onramp/mock-data/currencies.ts | 20 ++ .../features/onramp/mock-data/providers.ts | 27 +++ .../appkit-react/src/features/onramp/types.ts | 23 ++ .../swap/components/swap-field/swap-field.tsx | 4 +- packages/appkit-react/src/index.ts | 2 + packages/appkit-react/src/styles/index.css | 8 + .../src/styles/typography.module.css | 14 ++ 51 files changed, 1535 insertions(+), 126 deletions(-) rename packages/appkit-react/src/{features/balances => }/components/currency-item/currency-item.module.css (73%) rename packages/appkit-react/src/{features/balances => }/components/currency-item/currency-item.stories.tsx (87%) create mode 100644 packages/appkit-react/src/components/currency-item/currency-item.tsx rename packages/appkit-react/src/{features/balances => }/components/currency-item/index.ts (100%) rename packages/appkit-react/src/components/{token-select-modal/token-select-modal.module.css => currency-select-modal/currency-select-modal.module.css} (100%) create mode 100644 packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx create mode 100644 packages/appkit-react/src/components/currency-select-modal/index.ts rename packages/appkit-react/src/{features/swap => }/components/token-selector/index.ts (100%) rename packages/appkit-react/src/{features/swap => }/components/token-selector/token-selector.module.css (66%) rename packages/appkit-react/src/{features/swap => }/components/token-selector/token-selector.tsx (55%) delete mode 100644 packages/appkit-react/src/features/balances/components/currency-item/currency-item.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-input/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-presets/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/onramp-token-select-modal.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-token-selectors/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget-provider/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget-provider/onramp-widget-provider.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget-ui/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.stories.tsx create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.tsx create mode 100644 packages/appkit-react/src/features/onramp/index.ts create mode 100644 packages/appkit-react/src/features/onramp/mock-data/currencies.ts create mode 100644 packages/appkit-react/src/features/onramp/mock-data/providers.ts create mode 100644 packages/appkit-react/src/features/onramp/types.ts diff --git a/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx b/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx index 985958c7d..19754ea74 100644 --- a/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx +++ b/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx @@ -63,7 +63,7 @@ export const TokenListSheet: FC = ({ isOpen, onClose, onSel - + @@ -89,7 +89,7 @@ export const TokenListSheet: FC = ({ isOpen, onClose, onSel handleSelectJetton(jetton)} - style={styles.tokenItem} + style={styles.currencyItem} > {image ? ( @@ -143,7 +143,7 @@ const styles = StyleSheet.create(({ sizes, colors }, runtime) => ({ top: 0, right: 12, }, - tokenItem: { + currencyItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css b/packages/appkit-react/src/components/currency-item/currency-item.module.css similarity index 73% rename from packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css rename to packages/appkit-react/src/components/currency-item/currency-item.module.css index ac05553b9..17e2058bc 100644 --- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css +++ b/packages/appkit-react/src/components/currency-item/currency-item.module.css @@ -1,4 +1,4 @@ -.currencyItem { +.container { box-sizing: border-box; width: 100%; display: flex; @@ -39,7 +39,7 @@ } .name { - composes: bodySemibold from "../../../../styles/typography.module.css"; + composes: bodySemibold from "../../styles/typography.module.css"; color: var(--ta-color-text); white-space: nowrap; @@ -56,19 +56,26 @@ } .ticker { - composes: labelMedium from "../../../../styles/typography.module.css"; + composes: labelMedium from "../../styles/typography.module.css"; color: var(--ta-color-text-secondary); margin: 0; } -.details { +.rightSide { text-align: right; } -.balance { - composes: bodyMedium from "../../../../styles/typography.module.css"; +.mainBalance { + composes: bodyMedium from "../../styles/typography.module.css"; color: var(--ta-color-text); margin: 0; } + +.underBalance { + composes: labelMedium from "../../styles/typography.module.css"; + + color: var(--ta-color-text-secondary); + margin: 0; +} diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx b/packages/appkit-react/src/components/currency-item/currency-item.stories.tsx similarity index 87% rename from packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx rename to packages/appkit-react/src/components/currency-item/currency-item.stories.tsx index 1f59fbdc0..f665bfe1d 100644 --- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx +++ b/packages/appkit-react/src/components/currency-item/currency-item.stories.tsx @@ -12,7 +12,7 @@ import { fn } from 'storybook/test'; import { CurrencyItem } from './currency-item'; const meta: Meta = { - title: 'Public/Features/Balances/CurrencyItem', + title: 'Public/Components/CurrencyItem', component: CurrencyItem, tags: ['autodocs'], args: { @@ -71,6 +71,17 @@ export const NoBalance: Story = { }, }; +export const WithUnderBalance: Story = { + args: { + ticker: 'TON', + name: 'Toncoin', + balance: '55', + underBalance: '$385.00', + icon: 'https://ton.org/download/ton_symbol.png', + isVerified: true, + }, +}; + export const CurrencyList: Story = { render: () => (
diff --git a/packages/appkit-react/src/components/currency-item/currency-item.tsx b/packages/appkit-react/src/components/currency-item/currency-item.tsx new file mode 100644 index 000000000..4c9dad48f --- /dev/null +++ b/packages/appkit-react/src/components/currency-item/currency-item.tsx @@ -0,0 +1,137 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ComponentProps } from 'react'; +import clsx from 'clsx'; + +import { Logo } from '../logo'; +import type { LogoProps } from '../logo'; +import styles from './currency-item.module.css'; + +export interface CurrencyItemProps extends ComponentProps<'button'> { + ticker?: string; + name?: string; + balance?: string; + underBalance?: string; + icon?: string; + isVerified?: boolean; +} + +const Container: FC> = ({ className, children, ...props }) => ( + +); + +const LogoWrapper: FC = ({ className, ...props }) => ( + +); + +const Info: FC> = ({ className, children, ...props }) => ( +
+ {children} +
+); + +const Header: FC> = ({ className, children, ...props }) => ( +
+ {children} +
+); + +const Name: FC> = ({ className, children, ...props }) => ( +

+ {children} +

+); + +const Ticker: FC> = ({ className, children, ...props }) => ( +

+ {children} +

+); + +const VerifiedBadge: FC> = ({ className, ...props }) => ( + + + +); + +const RightSide: FC> = ({ className, children, ...props }) => ( +
+ {children} +
+); + +const MainBalance: FC> = ({ className, children, ...props }) => ( +

+ {children} +

+); + +const UnderBalance: FC> = ({ className, children, ...props }) => ( +

+ {children} +

+); + +const CurrencyItemRoot: FC = ({ + ticker, + name, + balance, + underBalance, + icon, + isVerified, + children, + ...props +}) => { + if (children) { + return {children}; + } + + return ( + + {(icon || ticker) && } + + +
+ {name || ticker} + {isVerified && } +
+ + + {ticker} {name && ticker && <>• {name}} + +
+ + {(balance || underBalance) && ( + + {balance && {balance}} + {underBalance && {underBalance}} + + )} +
+ ); +}; + +export const CurrencyItem = Object.assign(CurrencyItemRoot, { + Container, + Logo: LogoWrapper, + Info, + VerifiedBadge, + Header, + Name, + Ticker, + RightSide, + MainBalance, + UnderBalance, +}); diff --git a/packages/appkit-react/src/features/balances/components/currency-item/index.ts b/packages/appkit-react/src/components/currency-item/index.ts similarity index 100% rename from packages/appkit-react/src/features/balances/components/currency-item/index.ts rename to packages/appkit-react/src/components/currency-item/index.ts diff --git a/packages/appkit-react/src/components/token-select-modal/token-select-modal.module.css b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css similarity index 100% rename from packages/appkit-react/src/components/token-select-modal/token-select-modal.module.css rename to packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css diff --git a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx new file mode 100644 index 000000000..0f968be85 --- /dev/null +++ b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx @@ -0,0 +1,77 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ComponentProps } from 'react'; +import clsx from 'clsx'; + +import type { InputContainerProps } from '../input'; +import { Input } from '../input'; +import { Modal } from '../modal'; +import { SearchIcon } from '../search-icon'; +import styles from './currency-select-modal.module.css'; + +export interface CurrencySelectSearchProps extends Omit { + searchValue: string; + onSearchChange: (value: string) => void; + placeholder?: string; +} + +export const CurrencySelectSearch: FC = ({ + searchValue, + onSearchChange, + placeholder, + className, + ...props +}) => { + return ( + + + + + + + onSearchChange(e.target.value)} + autoFocus + /> + + + ); +}; + +export interface CurrencySelectListContainerProps extends ComponentProps<'div'> { + isEmpty: boolean; +} + +export const CurrencySelectListContainer: FC = ({ + isEmpty, + children, + className, + ...props +}) => { + return ( +
+ {isEmpty ? ( +
+

We didn't find any tokens.

+

Try searching by address.

+
+ ) : ( + children + )} +
+ ); +}; + +export const CurrencySelect = { + Modal, + Search: CurrencySelectSearch, + ListContainer: CurrencySelectListContainer, +}; diff --git a/packages/appkit-react/src/components/currency-select-modal/index.ts b/packages/appkit-react/src/components/currency-select-modal/index.ts new file mode 100644 index 000000000..255042d50 --- /dev/null +++ b/packages/appkit-react/src/components/currency-select-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { CurrencySelect } from './currency-select-modal'; +export type { CurrencySelectSearchProps, CurrencySelectListContainerProps } from './currency-select-modal'; diff --git a/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx b/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx index 395b042b5..3c8c3d501 100644 --- a/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx +++ b/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx @@ -10,12 +10,9 @@ import { useState } from 'react'; import type { FC } from 'react'; import { compareAddress } from '@ton/appkit'; -import { Input } from '../input/input'; -import { Modal } from '../modal/modal'; -import { SearchIcon } from '../search-icon'; -import { CurrencyItem } from '../../features/balances'; +import { CurrencySelect } from '../currency-select-modal'; +import { CurrencyItem } from '../currency-item'; import type { AppkitUIToken } from '../../types/appkit-ui-token'; -import styles from './token-select-modal.module.css'; export interface TokenSelectModalProps { open: boolean; @@ -57,41 +54,19 @@ export const TokenSelectModal: FC = ({ }; return ( - - - - - - - setSearch(e.target.value)} - autoFocus + + + + {filtered.map((token) => ( + - - - -
- {filtered.length === 0 ? ( -
-

We didn't find any tokens.

-

Try searching by address.

-
- ) : ( -
    - {filtered.map((token) => ( - - ))} -
- )} -
-
+ ))} + + ); }; diff --git a/packages/appkit-react/src/features/swap/components/token-selector/index.ts b/packages/appkit-react/src/components/token-selector/index.ts similarity index 100% rename from packages/appkit-react/src/features/swap/components/token-selector/index.ts rename to packages/appkit-react/src/components/token-selector/index.ts diff --git a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css b/packages/appkit-react/src/components/token-selector/token-selector.module.css similarity index 66% rename from packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css rename to packages/appkit-react/src/components/token-selector/token-selector.module.css index 4619cdc0a..df58efa40 100644 --- a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css +++ b/packages/appkit-react/src/components/token-selector/token-selector.module.css @@ -6,7 +6,11 @@ opacity: 0.8; } +.symbol { + margin-right: 2px; +} + .chevron { opacity: 0.5; - margin-left: 2px; + margin-left: auto; } diff --git a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx b/packages/appkit-react/src/components/token-selector/token-selector.tsx similarity index 55% rename from packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx rename to packages/appkit-react/src/components/token-selector/token-selector.tsx index 2f4cfadb0..bcec40847 100644 --- a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx +++ b/packages/appkit-react/src/components/token-selector/token-selector.tsx @@ -9,20 +9,23 @@ import type { FC } from 'react'; import styles from './token-selector.module.css'; -import { Button } from '../../../../components/button'; -import { Logo } from '../../../../components/logo'; +import { Button } from '../button'; +import type { ButtonProps } from '../button'; +import { Logo } from '../logo'; -export interface TokenSelectorProps { - symbol: string; +export interface TokenSelectorProps extends ButtonProps { + title: string; icon?: string; - onClick?: () => void; + iconFallback?: string; } -export const TokenSelector: FC = ({ symbol, icon, onClick }) => { +export const TokenSelector: FC = ({ title, icon, iconFallback, ...props }) => { return ( - - ); -}; diff --git a/packages/appkit-react/src/features/balances/index.ts b/packages/appkit-react/src/features/balances/index.ts index da4991e6d..e5a7a0f1d 100644 --- a/packages/appkit-react/src/features/balances/index.ts +++ b/packages/appkit-react/src/features/balances/index.ts @@ -6,7 +6,6 @@ * */ -export * from './components/currency-item'; export * from './components/balance-badge'; export * from './components/send-ton-button'; export * from './components/send-jetton-button'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/index.ts new file mode 100644 index 000000000..cebba3542 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampAmountInput } from './onramp-amount-input'; +export type { OnrampAmountInputProps } from './onramp-amount-input'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css new file mode 100644 index 000000000..d44353293 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css @@ -0,0 +1,57 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + cursor: text; + padding: 16px 0; + width: 100%; + overflow: hidden; +} + +.row { + display: flex; + align-items: baseline; + gap: 8px; + max-width: 100%; +} + +.input { + composes: inputXl from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); + border: none; + outline: none; + background: none; + padding: 0; + text-align: right; + min-width: 24px; + max-width: 100%; + box-sizing: content-box; +} + +.input::placeholder { + color: var(--ta-color-text-secondary); + opacity: 1; +} + +.ticker { + composes: inputXlSymbol from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); + white-space: nowrap; + user-select: none; +} + +.symbol { + composes: inputXl from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); + white-space: nowrap; + user-select: none; +} + +.mirror { + composes: inputXl from "../../../../styles/typography.module.css"; + position: absolute; + visibility: hidden; + white-space: nowrap; + pointer-events: none; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx new file mode 100644 index 000000000..27c28c7a5 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx @@ -0,0 +1,61 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useLayoutEffect, useRef, useState } from 'react'; +import type { FC } from 'react'; + +import styles from './onramp-amount-input.module.css'; + +export interface OnrampAmountInputProps { + value: string; + onChange: (value: string) => void; + ticker?: string; + symbol?: string; + placeholder?: string; +} + +export const OnrampAmountInput: FC = ({ + value, + onChange, + ticker, + symbol, + placeholder = '0', +}) => { + const mirrorRef = useRef(null); + const inputRef = useRef(null); + const [inputWidth, setInputWidth] = useState(undefined); + + useLayoutEffect(() => { + if (mirrorRef.current) { + setInputWidth(mirrorRef.current.offsetWidth + 2); + } + }, [value, placeholder]); + + return ( +
inputRef.current?.focus()}> +
+ {symbol && {symbol}} + onChange(e.target.value)} + style={{ width: inputWidth ? `${inputWidth}px` : undefined }} + /> + {ticker && {ticker}} +
+ + +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/index.ts new file mode 100644 index 000000000..44eaed2a6 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampAmountPresets } from './onramp-amount-presets'; +export type { OnrampAmountPresetsProps } from './onramp-amount-presets'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.module.css new file mode 100644 index 000000000..d63c053c8 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.module.css @@ -0,0 +1,25 @@ +.container { + width: fit-content; + display: grid; + gap: 8px; + justify-content: center; + grid-template-columns: 1fr 1fr 1fr 1fr; + margin: 0 auto; +} + +.preset { + width: 100%; + padding: 6px 16px; + /* border: 1px solid var(--ta-color-border); */ + /* background: none; */ + cursor: pointer; + /* border-radius: var(--ta-border-radius-m); */ + /* color: var(--ta-color-text); */ + /* transition: background 0.15s, border-color 0.15s; */ + white-space: nowrap; +} + +.preset:hover { + background: var(--ta-color-bg-secondary); + border-color: var(--ta-color-text-secondary); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx new file mode 100644 index 000000000..6d3e2906f --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { Button } from '../../../../components/button'; +import styles from './onramp-amount-presets.module.css'; + +export interface OnrampAmountPresetsProps { + presets: number[]; + currencySymbol: string; + onSelect: (value: number) => void; +} + +export const OnrampAmountPresets: FC = ({ presets, currencySymbol, onSelect }) => { + return ( +
+ {presets.map((value) => ( + + ))} +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts new file mode 100644 index 000000000..c58efcbbb --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampCheckout } from './onramp-checkout'; +export type { OnrampCheckoutProps } from './onramp-checkout'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css new file mode 100644 index 000000000..c00c0ca24 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css @@ -0,0 +1,41 @@ +.content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.amountSection { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 24px 0; +} + +.tokenAmount { + composes: display from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} + +.fiatAmount { + composes: bodyRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} + +.providerRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-top: 1px solid var(--ta-color-border); +} + +.providerLabel { + composes: bodyRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} + +.providerName { + composes: bodyMedium from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx new file mode 100644 index 000000000..75fedb25d --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx @@ -0,0 +1,65 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { Modal } from '../../../../components/modal'; +import { Button } from '../../../../components/button'; +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import type { OnrampProvider } from '../../types'; +import styles from './onramp-checkout.module.css'; + +export interface OnrampCheckoutProps { + open: boolean; + onClose: () => void; + token: AppkitUIToken | null; + amount: string; + fiatAmount: string; + fiatSymbol: string; + provider: OnrampProvider | null; + isPurchasing: boolean; + onConfirm: () => void; +} + +export const OnrampCheckout: FC = ({ + open, + onClose, + token, + amount, + fiatAmount, + fiatSymbol, + provider, + isPurchasing, + onConfirm, +}) => { + return ( + !isOpen && onClose()} title="Checkout"> +
+
+ + {amount} {token?.symbol} + + + {fiatSymbol} {fiatAmount} + +
+ + {provider && ( +
+ Provider + {provider.name} +
+ )} + + +
+
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts new file mode 100644 index 000000000..fe67367b2 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampCurrencyItem } from './onramp-currency-item'; +export type { OnrampCurrencyItemProps } from './onramp-currency-item'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx new file mode 100644 index 000000000..f86279c5d --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ComponentProps, FC } from 'react'; + +import { CurrencyItem } from '../../../../components/currency-item'; +import type { OnrampCurrency } from '../../types'; + +export interface OnrampCurrencyItemProps extends ComponentProps { + currency: OnrampCurrency; +} + +export const OnrampCurrencyItem: FC = ({ currency }) => { + return ( + + + + + {currency.name} + + + {currency.code} + + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts new file mode 100644 index 000000000..58d97ad04 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampCurrencySelectModal } from './onramp-currency-select-modal'; +export type { OnrampCurrencySelectModalProps } from './onramp-currency-select-modal'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx new file mode 100644 index 000000000..9b60d7fcc --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState } from 'react'; +import type { FC } from 'react'; + +import { CurrencySelect } from '../../../../components/currency-select-modal'; +import type { OnrampCurrency } from '../../types'; +import { OnrampCurrencyItem } from '../onramp-currency-item'; + +export interface OnrampCurrencySelectModalProps { + open: boolean; + onClose: () => void; + currencies: OnrampCurrency[]; + onSelect: (currency: OnrampCurrency) => void; +} + +export const OnrampCurrencySelectModal: FC = ({ + open, + onClose, + currencies, + onSelect, +}) => { + const [search, setSearch] = useState(''); + + const filtered = currencies.filter( + (c) => + c.name.toLowerCase().includes(search.toLowerCase()) || c.code.toLowerCase().includes(search.toLowerCase()), + ); + + const handleSelect = (currency: OnrampCurrency) => () => { + onSelect(currency); + onClose(); + setSearch(''); + }; + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + onClose(); + setSearch(''); + } + }; + + return ( + + + + + {filtered.map((currency) => ( + + ))} + + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts new file mode 100644 index 000000000..2853976e1 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampProviderSelect } from './onramp-provider-select'; +export type { OnrampProviderSelectProps } from './onramp-provider-select'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css new file mode 100644 index 000000000..78760e714 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css @@ -0,0 +1,54 @@ +.list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 12px 8px; + border: none; + background: none; + cursor: pointer; + border-radius: var(--ta-border-radius-m); + text-align: left; + transition: background 0.15s; +} + +.item:hover { + background: var(--ta-color-bg-secondary); +} + +.logo { + composes: labelSemibold from "../../../../styles/typography.module.css"; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--ta-color-bg-secondary); + color: var(--ta-color-text); + flex-shrink: 0; +} + +.info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.name { + composes: bodyMedium from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} + +.description { + composes: labelRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx new file mode 100644 index 000000000..931c39491 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { Modal } from '../../../../components/modal'; +import type { OnrampProvider } from '../../types'; +import styles from './onramp-provider-select.module.css'; + +export interface OnrampProviderSelectProps { + open: boolean; + onClose: () => void; + providers: OnrampProvider[]; + onSelect: (provider: OnrampProvider) => void; +} + +export const OnrampProviderSelect: FC = ({ open, onClose, providers, onSelect }) => { + const handleSelect = (provider: OnrampProvider) => () => { + onSelect(provider); + onClose(); + }; + + return ( + !isOpen && onClose()} title="Method of purchase"> +
    + {providers.map((provider) => ( +
  • + +
  • + ))} +
+
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/index.ts new file mode 100644 index 000000000..4a7f2b49b --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampTokenSelectModal } from './onramp-token-select-modal'; +export type { OnrampTokenSelectModalProps } from './onramp-token-select-modal'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/onramp-token-select-modal.tsx b/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/onramp-token-select-modal.tsx new file mode 100644 index 000000000..15bccbe4a --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-select-modal/onramp-token-select-modal.tsx @@ -0,0 +1,18 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { TokenSelectModal } from '../../../../components/token-select-modal'; +import type { TokenSelectModalProps } from '../../../../components/token-select-modal'; + +export type OnrampTokenSelectModalProps = Omit; + +export const OnrampTokenSelectModal: FC = (props) => { + return ; +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/index.ts new file mode 100644 index 000000000..c8a0ddb8a --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampTokenSelectors } from './onramp-token-selectors'; +export type { OnrampTokenSelectorsProps } from './onramp-token-selectors'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.module.css b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.module.css new file mode 100644 index 000000000..78a94fcc4 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.module.css @@ -0,0 +1,11 @@ +.container { + display: grid; + gap: 4px; + padding: 2px; + width: 100%; + grid-template-columns: 1fr 1fr; +} + +.tokenSelector { + width: 100%; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx new file mode 100644 index 000000000..d54cf4900 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ComponentProps } from 'react'; +import clsx from 'clsx'; + +import styles from './onramp-token-selectors.module.css'; +import { TokenSelector } from '../../../../components/token-selector'; + +export interface OnrampTokenSelectorsProps extends ComponentProps<'div'> { + from: { title: string; logoSrc?: string }; + to: { title: string; logoSrc?: string }; + onFromClick: () => void; + onToClick: () => void; +} + +export const OnrampTokenSelectors: FC = ({ + from, + to, + onFromClick, + onToClick, + className, + ...props +}) => { + return ( +
+ + + +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/index.ts new file mode 100644 index 000000000..2b79e6063 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampWidgetProvider, useOnrampContext } from './onramp-widget-provider'; +export type { OnrampProviderProps, OnrampContextType } from './onramp-widget-provider'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/onramp-widget-provider.tsx b/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/onramp-widget-provider.tsx new file mode 100644 index 000000000..bcc4b045d --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-provider/onramp-widget-provider.tsx @@ -0,0 +1,203 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import type { OnrampCurrency, OnrampProvider, AmountInputMode } from '../../types'; +import { ONRAMP_CURRENCIES } from '../../mock-data/currencies'; +import { ONRAMP_PROVIDERS } from '../../mock-data/providers'; + +export type { AppkitUIToken }; + +const MOCK_RATE = 0.82; +const ERROR_THRESHOLD = 10000; +const DEFAULT_PRESETS = [100, 250, 500, 1000]; + +export interface OnrampContextType { + /** Full list of available tokens to buy */ + tokens: AppkitUIToken[]; + /** Currently selected token to buy */ + selectedToken: AppkitUIToken | null; + /** Select a token to buy */ + setSelectedToken: (token: AppkitUIToken) => void; + + /** Available fiat currencies */ + currencies: OnrampCurrency[]; + /** Currently selected fiat currency */ + selectedCurrency: OnrampCurrency; + /** Select a fiat currency */ + setSelectedCurrency: (currency: OnrampCurrency) => void; + + /** Current amount input value */ + amount: string; + /** Set the amount value */ + setAmount: (value: string) => void; + /** Whether user is entering token amount or fiat amount */ + amountInputMode: AmountInputMode; + /** Switch between token / currency input mode */ + setAmountInputMode: (mode: AmountInputMode) => void; + /** Mocked converted amount in the opposite denomination */ + convertedAmount: string; + /** Preset amount values */ + presetAmounts: number[]; + /** Set amount from a preset value */ + setPresetAmount: (value: number) => void; + + /** Available payment providers */ + providers: OnrampProvider[]; + /** Currently selected provider */ + selectedProvider: OnrampProvider | null; + /** Select a payment provider */ + setSelectedProvider: (provider: OnrampProvider) => void; + + /** Whether amount is valid and user can proceed */ + canContinue: boolean; + /** Current error, e.g. 'noQuotesFound' */ + error: string | null; + /** Whether a purchase is in progress (mocked) */ + isPurchasing: boolean; + /** Start the purchase (mocked) */ + onPurchase: () => void; + /** Reset widget to initial state */ + onReset: () => void; +} + +const defaultContext: OnrampContextType = { + tokens: [], + selectedToken: null, + setSelectedToken: () => {}, + currencies: [], + selectedCurrency: ONRAMP_CURRENCIES[0]!, + setSelectedCurrency: () => {}, + amount: '', + setAmount: () => {}, + amountInputMode: 'token', + setAmountInputMode: () => {}, + convertedAmount: '', + presetAmounts: DEFAULT_PRESETS, + setPresetAmount: () => {}, + providers: [], + selectedProvider: null, + setSelectedProvider: () => {}, + canContinue: false, + error: null, + isPurchasing: false, + onPurchase: () => {}, + onReset: () => {}, +}; + +export const OnrampContext = createContext(defaultContext); + +export function useOnrampContext() { + return useContext(OnrampContext); +} + +export interface OnrampProviderProps extends PropsWithChildren { + /** Full list of tokens available for purchase */ + tokens: AppkitUIToken[]; + /** Symbol of the token pre-selected for purchase */ + defaultTokenSymbol?: string; + /** Code of the fiat currency pre-selected */ + defaultCurrencyCode?: string; +} + +export const OnrampWidgetProvider: FC = ({ + children, + tokens, + defaultTokenSymbol, + defaultCurrencyCode, +}) => { + const [selectedToken, setSelectedToken] = useState( + () => tokens.find((t) => t.symbol === defaultTokenSymbol) ?? tokens[0] ?? null, + ); + + const [selectedCurrency, setSelectedCurrency] = useState( + () => ONRAMP_CURRENCIES.find((c) => c.code === defaultCurrencyCode) ?? ONRAMP_CURRENCIES[0]!, + ); + + const [amount, setAmount] = useState(''); + const [amountInputMode, setAmountInputMode] = useState('token'); + const [selectedProvider, setSelectedProvider] = useState(null); + const [isPurchasing, setIsPurchasing] = useState(false); + + const setPresetAmount = useCallback((value: number) => { + setAmount(String(value)); + }, []); + + const convertedAmount = useMemo(() => { + const num = parseFloat(amount); + if (!amount || isNaN(num)) return ''; + if (amountInputMode === 'token') { + return (num * MOCK_RATE).toFixed(2); + } + return (num / MOCK_RATE).toFixed(2); + }, [amount, amountInputMode]); + + const numericAmount = parseFloat(amount); + const error = !isNaN(numericAmount) && numericAmount > ERROR_THRESHOLD ? 'noQuotesFound' : null; + const canContinue = amount !== '' && !isNaN(numericAmount) && numericAmount > 0 && error === null; + + const onPurchase = useCallback(() => { + setIsPurchasing(true); + setTimeout(() => { + setIsPurchasing(false); + }, 1500); + }, []); + + const onReset = useCallback(() => { + setAmount(''); + setSelectedProvider(null); + setAmountInputMode('token'); + setIsPurchasing(false); + }, []); + + const value = useMemo( + () => ({ + tokens, + selectedToken, + setSelectedToken, + currencies: ONRAMP_CURRENCIES, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + convertedAmount, + presetAmounts: DEFAULT_PRESETS, + setPresetAmount, + providers: ONRAMP_PROVIDERS, + selectedProvider, + setSelectedProvider, + canContinue, + error, + isPurchasing, + onPurchase, + onReset, + }), + [ + tokens, + selectedToken, + selectedCurrency, + amount, + amountInputMode, + convertedAmount, + setPresetAmount, + selectedProvider, + canContinue, + error, + isPurchasing, + onPurchase, + onReset, + ], + ); + + return {children}; +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/index.ts new file mode 100644 index 000000000..dd9854310 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampWidgetUI } from './onramp-widget-ui'; +export type { OnrampWidgetRenderProps } from './onramp-widget-ui'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css new file mode 100644 index 000000000..3e48b1750 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css @@ -0,0 +1,23 @@ +.widget { + box-sizing: border-box; + width: 100%; + max-width: 340px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.tabsRow { + display: flex; + justify-content: center; +} + +.convertedLine { + composes: bodyRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); + text-align: center; +} + +.error { + color: var(--ta-color-negative); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx new file mode 100644 index 000000000..82f032422 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx @@ -0,0 +1,144 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useState } from 'react'; +import type { FC } from 'react'; + +import { Button } from '../../../../components/button'; +import type { OnrampContextType } from '../onramp-widget-provider'; +import { OnrampTokenSelectors } from '../onramp-token-selectors'; +import { OnrampAmountInput } from '../onramp-amount-input'; +import { OnrampAmountPresets } from '../onramp-amount-presets'; +import { OnrampTokenSelectModal } from '../onramp-token-select-modal'; +import { OnrampCurrencySelectModal } from '../onramp-currency-select-modal'; +import { OnrampProviderSelect } from '../onramp-provider-select'; +import { OnrampCheckout } from '../onramp-checkout'; +import styles from './onramp-widget-ui.module.css'; + +export type OnrampWidgetRenderProps = OnrampContextType; + +export const OnrampWidgetUI: FC = ({ + tokens, + selectedToken, + setSelectedToken, + currencies, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + // setAmountInputMode, + convertedAmount, + presetAmounts, + setPresetAmount, + providers, + selectedProvider, + setSelectedProvider, + canContinue, + error, + isPurchasing, + onPurchase, +}) => { + const [isTokenSelectOpen, setIsTokenSelectOpen] = useState(false); + const [isCurrencySelectOpen, setIsCurrencySelectOpen] = useState(false); + const [isProviderSelectOpen, setIsProviderSelectOpen] = useState(false); + const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); + + const convertedSymbol = amountInputMode === 'token' ? selectedCurrency.symbol : (selectedToken?.symbol ?? ''); + + const handleContinue = useCallback(() => { + if (selectedProvider) { + setIsCheckoutOpen(true); + } else { + setIsProviderSelectOpen(true); + } + }, [selectedProvider]); + + const handleProviderSelected = useCallback( + (provider: typeof selectedProvider) => { + if (provider) { + setSelectedProvider(provider); + setIsCheckoutOpen(true); + } + }, + [setSelectedProvider], + ); + + const fiatAmount = amountInputMode === 'token' ? convertedAmount : amount; + + return ( +
+
+ setIsTokenSelectOpen(true)} + onToClick={() => setIsCurrencySelectOpen(true)} + /> +
+ + + +
+ {error === 'noQuotesFound' ? ( + No quotes found + ) : ( + convertedAmount && `${convertedSymbol} ${convertedAmount}` + )} +
+ + + + + + setIsTokenSelectOpen(false)} + tokens={tokens} + onSelect={setSelectedToken} + /> + + setIsCurrencySelectOpen(false)} + currencies={currencies} + onSelect={setSelectedCurrency} + /> + + setIsProviderSelectOpen(false)} + providers={providers} + onSelect={handleProviderSelected} + /> + + setIsCheckoutOpen(false)} + token={selectedToken} + amount={amountInputMode === 'token' ? amount : convertedAmount} + fiatAmount={fiatAmount} + fiatSymbol={selectedCurrency.symbol} + provider={selectedProvider} + isPurchasing={isPurchasing} + onConfirm={onPurchase} + /> +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-widget/index.ts new file mode 100644 index 000000000..8c8b72d43 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampWidget } from './onramp-widget'; +export type { OnrampWidgetProps } from './onramp-widget'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.stories.tsx b/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.stories.tsx new file mode 100644 index 000000000..469f62931 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.stories.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { STORY_TOKENS } from '../../../../storybook/fixtures/tokens'; +import { OnrampWidget } from './onramp-widget'; + +const meta: Meta = { + title: 'Public/Features/Onramp/OnrampWidget', + component: OnrampWidget, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + tokens: STORY_TOKENS, + defaultTokenSymbol: 'USD₮', + defaultCurrencyCode: 'EUR', + }, +}; + +export const CustomUI: Story = { + args: { + tokens: STORY_TOKENS, + defaultTokenSymbol: 'TON', + defaultCurrencyCode: 'USD', + }, + render: (args) => ( + + {({ selectedToken, selectedCurrency, amount, setAmount, canContinue }) => ( +
+
+ Buy {selectedToken?.symbol} with {selectedCurrency.code} +
+ setAmount(e.target.value)} + placeholder="0" + inputMode="decimal" + style={{ fontSize: 32, fontWeight: 'bold', border: 'none', outline: 'none' }} + /> + +
+ )} +
+ ), +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.tsx b/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.tsx new file mode 100644 index 000000000..d13962b8a --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget/onramp-widget.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ReactNode } from 'react'; + +import type { OnrampWidgetRenderProps } from '../onramp-widget-ui'; +import { OnrampWidgetUI } from '../onramp-widget-ui'; +import { OnrampWidgetProvider, useOnrampContext } from '../onramp-widget-provider'; +import type { OnrampProviderProps } from '../onramp-widget-provider'; + +export interface OnrampWidgetProps extends Omit { + /** Custom render function — when provided, replaces the default widget UI */ + children?: (props: OnrampWidgetRenderProps) => ReactNode; +} + +const OnrampWidgetContent: FC<{ children?: (props: OnrampWidgetRenderProps) => ReactNode }> = ({ children }) => { + const ctx = useOnrampContext(); + + if (children) { + return <>{children(ctx)}; + } + + return ; +}; + +export const OnrampWidget: FC = ({ children, tokens, defaultTokenSymbol, defaultCurrencyCode }) => { + return ( + + {children} + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/index.ts b/packages/appkit-react/src/features/onramp/index.ts new file mode 100644 index 000000000..05d8d4f5c --- /dev/null +++ b/packages/appkit-react/src/features/onramp/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './components/onramp-widget'; +export * from './components/onramp-widget-provider'; +export * from './types'; diff --git a/packages/appkit-react/src/features/onramp/mock-data/currencies.ts b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts new file mode 100644 index 000000000..8e33d5270 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { OnrampCurrency } from '../types'; + +export const ONRAMP_CURRENCIES: OnrampCurrency[] = [ + { code: 'EUR', name: 'Euro', symbol: '\u20AC', flag: 'https://static.moonpay.com/widget/currencies/eur.svg' }, + { code: 'USD', name: 'US Dollar', symbol: '$', flag: 'https://static.moonpay.com/widget/currencies/usd.svg' }, + { + code: 'GBP', + name: 'Pound Sterling', + symbol: '\u00A3', + flag: 'https://static.moonpay.com/widget/currencies/gbp.svg', + }, +]; diff --git a/packages/appkit-react/src/features/onramp/mock-data/providers.ts b/packages/appkit-react/src/features/onramp/mock-data/providers.ts new file mode 100644 index 000000000..4ba3ce734 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/mock-data/providers.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { OnrampProvider } from '../types'; + +export const ONRAMP_PROVIDERS: OnrampProvider[] = [ + { + id: 'moonpay', + name: 'MoonPay', + description: 'Visa, Mastercard, Apple Pay, Google Pay, SEPA', + }, + { + id: 'transak', + name: 'Transak', + description: 'Visa, Mastercard, Apple Pay, Google Pay, SEPA', + }, + { + id: 'binance', + name: 'Binance', + description: 'Visa, Mastercard, Apple Pay, Binance Card', + }, +]; diff --git a/packages/appkit-react/src/features/onramp/types.ts b/packages/appkit-react/src/features/onramp/types.ts new file mode 100644 index 000000000..98a113865 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/types.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface OnrampCurrency { + code: string; + name: string; + symbol: string; + flag?: string; +} + +export interface OnrampProvider { + id: string; + name: string; + description?: string; + logo?: string; +} + +export type AmountInputMode = 'token' | 'currency'; diff --git a/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx index ef7fc7400..61ee92c8f 100644 --- a/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx +++ b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx @@ -12,7 +12,7 @@ import { calcFiatValue, formatLargeValue } from '@ton/appkit'; import { useI18n } from '../../../settings/hooks/use-i18n'; import { Input } from '../../../../components/input/input'; import { Skeleton } from '../../../../components/skeleton'; -import { TokenSelector } from '../token-selector/token-selector'; +import { TokenSelector } from '../../../../components/token-selector'; import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; import { useSwapContext } from '../swap-widget-provider/swap-widget-provider'; import styles from './swap-field.module.css'; @@ -60,7 +60,7 @@ export const SwapField: FC = ({ disabled={type === 'receive'} /> - + diff --git a/packages/appkit-react/src/index.ts b/packages/appkit-react/src/index.ts index 7c1acf378..e4772dc27 100644 --- a/packages/appkit-react/src/index.ts +++ b/packages/appkit-react/src/index.ts @@ -20,6 +20,7 @@ export * from './components/skeleton'; export * from './components/ton-icon'; export * from './components/input'; export * from './components/token-select-modal'; +export * from './components/currency-item'; export * from './features/balances'; export * from './features/jettons'; @@ -31,5 +32,6 @@ export * from './features/settings'; export * from './features/swap'; export * from './features/signing'; export * from './features/staking'; +export * from './features/onramp'; export * from './types/appkit-ui-token'; diff --git a/packages/appkit-react/src/styles/index.css b/packages/appkit-react/src/styles/index.css index e4b841aa6..464edf4a1 100644 --- a/packages/appkit-react/src/styles/index.css +++ b/packages/appkit-react/src/styles/index.css @@ -62,6 +62,14 @@ --ta-input-l-weight: 700; --ta-input-l-line-height: 40px; + --ta-input-xl-size: 60px; + --ta-input-xl-weight: 700; + --ta-input-xl-line-height: 78px; + + --ta-input-xl-symbol-size: 40px; + --ta-input-xl-symbol-weight: 700; + --ta-input-xl-symbol-line-height: 100%; + /* Colors */ --ta-color-primary: #007AFF; --ta-color-primary-foreground: #FFFFFF; diff --git a/packages/appkit-react/src/styles/typography.module.css b/packages/appkit-react/src/styles/typography.module.css index 8c0e0d460..076a52de9 100644 --- a/packages/appkit-react/src/styles/typography.module.css +++ b/packages/appkit-react/src/styles/typography.module.css @@ -126,3 +126,17 @@ font-weight: var(--ta-input-l-weight); line-height: var(--ta-input-l-line-height); } + +.inputXl { + font-family: var(--ta-font-family); + font-size: var(--ta-input-xl-size); + font-weight: var(--ta-input-xl-weight); + line-height: var(--ta-input-xl-line-height); +} + +.inputXlSymbol { + font-family: var(--ta-font-family); + font-size: var(--ta-input-xl-symbol-size); + font-weight: var(--ta-input-xl-symbol-weight); + line-height: var(--ta-input-xl-symbol-line-height); +} From e0e4c33a8cf04a1cd1a7fbfa6108e12426e97928 Mon Sep 17 00:00:00 2001 From: VK Date: Tue, 7 Apr 2026 17:23:30 +0400 Subject: [PATCH 04/67] feat(onramp): reword design --- .../currency-select-modal.module.css | 4 + .../currency-select-modal.tsx | 7 +- .../appkit-react/src/components/logo/logo.tsx | 6 +- .../src/components/modal/modal.tsx | 8 +- .../onramp-amount-input.module.css | 9 +- .../onramp-amount-input.tsx | 15 ++-- .../onramp-amount-presets.tsx | 30 ++++--- .../onramp-amount-reversed/index.ts | 10 +++ .../onramp-amount-reversed.module.css | 28 ++++++ .../onramp-amount-reversed.tsx | 68 ++++++++++++++ .../onramp-checkout.module.css | 41 --------- .../onramp-checkout/onramp-checkout.tsx | 65 -------------- .../onramp-currency-item.tsx | 6 +- .../index.ts | 4 +- .../onramp-provider-item.module.css | 25 ++++++ .../onramp-provider-item.tsx | 36 ++++++++ .../onramp-provider-select.module.css | 48 +--------- .../onramp-provider-select.tsx | 19 ++-- .../onramp-token-selectors.tsx | 2 + .../onramp-widget-provider.tsx | 46 ++++------ .../onramp-widget-ui.module.css | 22 ++--- .../onramp-widget-ui/onramp-widget-ui.tsx | 88 +++++++------------ .../features/onramp/mock-data/currencies.ts | 6 +- .../features/onramp/mock-data/providers.ts | 9 +- .../appkit-react/src/features/onramp/types.ts | 9 +- 25 files changed, 305 insertions(+), 306 deletions(-) create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx delete mode 100644 packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css delete mode 100644 packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx rename packages/appkit-react/src/features/onramp/components/{onramp-checkout => onramp-provider-item}/index.ts (55%) create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css create mode 100644 packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx diff --git a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css index 8f8e2d427..739d7f2ab 100644 --- a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css +++ b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.module.css @@ -2,6 +2,10 @@ margin-bottom: 16px; } +.body { + overflow-y: hidden; +} + .list { list-style: none; margin: 0; diff --git a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx index 0f968be85..0979aaafb 100644 --- a/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx +++ b/packages/appkit-react/src/components/currency-select-modal/currency-select-modal.tsx @@ -11,6 +11,7 @@ import clsx from 'clsx'; import type { InputContainerProps } from '../input'; import { Input } from '../input'; +import type { ModalProps } from '../modal'; import { Modal } from '../modal'; import { SearchIcon } from '../search-icon'; import styles from './currency-select-modal.module.css'; @@ -70,8 +71,12 @@ export const CurrencySelectListContainer: FC = ); }; +export const CurrencySelectModal: FC = ({ className, ...props }) => { + return ; +}; + export const CurrencySelect = { - Modal, + Modal: CurrencySelectModal, Search: CurrencySelectSearch, ListContainer: CurrencySelectListContainer, }; diff --git a/packages/appkit-react/src/components/logo/logo.tsx b/packages/appkit-react/src/components/logo/logo.tsx index b325bf4f7..1a2bf3c04 100644 --- a/packages/appkit-react/src/components/logo/logo.tsx +++ b/packages/appkit-react/src/components/logo/logo.tsx @@ -95,7 +95,11 @@ export interface LogoProps extends ComponentPropsWithoutRef<'span'> { export const Logo = forwardRef, LogoProps>(({ size = 30, src, alt, fallback, ...props }, ref) => { return ( - + {(fallback || alt) && {fallback ? fallback : alt?.[0]}} diff --git a/packages/appkit-react/src/components/modal/modal.tsx b/packages/appkit-react/src/components/modal/modal.tsx index c6fdb290d..e2c848730 100644 --- a/packages/appkit-react/src/components/modal/modal.tsx +++ b/packages/appkit-react/src/components/modal/modal.tsx @@ -33,6 +33,10 @@ export interface ModalProps { * Additional class name for the content container. */ className?: string; + /** + * Additional class name for the body container. + */ + bodyClassName?: string; } const CloseIcon = () => ( @@ -47,13 +51,13 @@ const CloseIcon = () => ( ); -export const Modal: FC = ({ open, onOpenChange, title, children, className }) => { +export const Modal: FC = ({ open, onOpenChange, title, children, className, bodyClassName }) => { return ( onOpenChange?.(false)}> e.stopPropagation()}> -
+
{title && {title}} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css index d44353293..42121dead 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css @@ -4,7 +4,6 @@ align-items: center; position: relative; cursor: text; - padding: 16px 0; width: 100%; overflow: hidden; } @@ -12,7 +11,6 @@ .row { display: flex; align-items: baseline; - gap: 8px; max-width: 100%; } @@ -30,20 +28,21 @@ } .input::placeholder { - color: var(--ta-color-text-secondary); + color: var(--ta-color-text-tertiary); opacity: 1; } .ticker { composes: inputXlSymbol from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); + color: var(--ta-color-text-tertiary); white-space: nowrap; user-select: none; + margin-left: 8px; } .symbol { composes: inputXl from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); + color: var(--ta-color-text-tertiary); white-space: nowrap; user-select: none; } diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx index 27c28c7a5..f14ee72b4 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx @@ -7,13 +7,14 @@ */ import { useLayoutEffect, useRef, useState } from 'react'; -import type { FC } from 'react'; +import type { FC, ComponentProps } from 'react'; +import clsx from 'clsx'; import styles from './onramp-amount-input.module.css'; -export interface OnrampAmountInputProps { +export interface OnrampAmountInputProps extends ComponentProps<'div'> { value: string; - onChange: (value: string) => void; + onValueChange: (value: string) => void; ticker?: string; symbol?: string; placeholder?: string; @@ -21,10 +22,12 @@ export interface OnrampAmountInputProps { export const OnrampAmountInput: FC = ({ value, - onChange, + onValueChange, ticker, symbol, placeholder = '0', + className, + ...props }) => { const mirrorRef = useRef(null); const inputRef = useRef(null); @@ -37,7 +40,7 @@ export const OnrampAmountInput: FC = ({ }, [value, placeholder]); return ( -
inputRef.current?.focus()}> +
inputRef.current?.focus()} {...props}>
{symbol && {symbol}} = ({ inputMode="decimal" placeholder={placeholder} value={value} - onChange={(e) => onChange(e.target.value)} + onChange={(e) => onValueChange(e.target.value)} style={{ width: inputWidth ? `${inputWidth}px` : undefined }} /> {ticker && {ticker}} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx index 6d3e2906f..746e8e6cf 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-presets/onramp-amount-presets.tsx @@ -6,30 +6,38 @@ * */ -import type { FC } from 'react'; +import type { ComponentProps, FC } from 'react'; +import clsx from 'clsx'; import { Button } from '../../../../components/button'; import styles from './onramp-amount-presets.module.css'; +import type { OnrampAmountPreset } from '../../types'; -export interface OnrampAmountPresetsProps { - presets: number[]; - currencySymbol: string; - onSelect: (value: number) => void; +export interface OnrampAmountPresetsProps extends ComponentProps<'div'> { + presets: OnrampAmountPreset[]; + currencySymbol?: string; + onPresetSelect: (value: string) => void; } -export const OnrampAmountPresets: FC = ({ presets, currencySymbol, onSelect }) => { +export const OnrampAmountPresets: FC = ({ + presets, + currencySymbol, + onPresetSelect, + className, + ...props +}) => { return ( -
- {presets.map((value) => ( +
+ {presets.map((preset) => ( ))}
diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts new file mode 100644 index 000000000..d07d5150b --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { OnrampAmountReversed } from './onramp-amount-reversed'; +export type { OnrampAmountReversedProps } from './onramp-amount-reversed'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css new file mode 100644 index 000000000..07bacc131 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css @@ -0,0 +1,28 @@ +.container { + composes: bodySemibold from '../../../../styles/typography.module.css'; + + display: flex; + align-items: center; + justify-content: center; + cursor: text; + width: 100%; + overflow: hidden; + color: var(--ta-color-text-secondary); + gap: 8px; +} + +.changeDirection { + width: 14px; + height: 14px; + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; + outline: none; + color: var(--ta-color-text); + transition: opacity 0.2s ease-in-out; +} + +.changeDirection:hover { + opacity: 0.8; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx new file mode 100644 index 000000000..0c8f1db39 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ComponentProps, FC } from 'react'; +import { formatLargeValue } from '@ton/appkit'; +import clsx from 'clsx'; + +import styles from './onramp-amount-reversed.module.css'; + +export interface OnrampAmountReversedProps extends ComponentProps<'div'> { + value: string; + onChangeDirection: () => void; + ticker?: string; + symbol?: string; + errorMessage?: string; +} + +export const OnrampAmountReversed: FC = ({ + value, + onChangeDirection, + ticker, + symbol, + errorMessage, + className, + ...props +}) => { + if (errorMessage) { + return ( +
+ {errorMessage} +
+ ); + } + + return ( +
+ + {symbol} + {value ? formatLargeValue(value, 2) : '0'} + {ticker ? ` ${ticker}` : ''} + + + +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css deleted file mode 100644 index c00c0ca24..000000000 --- a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.content { - display: flex; - flex-direction: column; - gap: 16px; -} - -.amountSection { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 24px 0; -} - -.tokenAmount { - composes: display from "../../../../styles/typography.module.css"; - color: var(--ta-color-text); -} - -.fiatAmount { - composes: bodyRegular from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); -} - -.providerRow { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 0; - border-top: 1px solid var(--ta-color-border); -} - -.providerLabel { - composes: bodyRegular from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); -} - -.providerName { - composes: bodyMedium from "../../../../styles/typography.module.css"; - color: var(--ta-color-text); -} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx b/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx deleted file mode 100644 index 75fedb25d..000000000 --- a/packages/appkit-react/src/features/onramp/components/onramp-checkout/onramp-checkout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { FC } from 'react'; - -import { Modal } from '../../../../components/modal'; -import { Button } from '../../../../components/button'; -import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; -import type { OnrampProvider } from '../../types'; -import styles from './onramp-checkout.module.css'; - -export interface OnrampCheckoutProps { - open: boolean; - onClose: () => void; - token: AppkitUIToken | null; - amount: string; - fiatAmount: string; - fiatSymbol: string; - provider: OnrampProvider | null; - isPurchasing: boolean; - onConfirm: () => void; -} - -export const OnrampCheckout: FC = ({ - open, - onClose, - token, - amount, - fiatAmount, - fiatSymbol, - provider, - isPurchasing, - onConfirm, -}) => { - return ( - !isOpen && onClose()} title="Checkout"> -
-
- - {amount} {token?.symbol} - - - {fiatSymbol} {fiatAmount} - -
- - {provider && ( -
- Provider - {provider.name} -
- )} - - -
-
- ); -}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx index f86279c5d..78eef346e 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx @@ -15,10 +15,10 @@ export interface OnrampCurrencyItemProps extends ComponentProps = ({ currency }) => { +export const OnrampCurrencyItem: FC = ({ currency, ...props }) => { return ( - - + + {currency.name} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/index.ts similarity index 55% rename from packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts rename to packages/appkit-react/src/features/onramp/components/onramp-provider-item/index.ts index c58efcbbb..0213d7109 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-checkout/index.ts +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/index.ts @@ -6,5 +6,5 @@ * */ -export { OnrampCheckout } from './onramp-checkout'; -export type { OnrampCheckoutProps } from './onramp-checkout'; +export { OnrampProviderItem } from './onramp-provider-item'; +export type { OnrampProviderItemProps } from './onramp-provider-item'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css new file mode 100644 index 000000000..815d8f0bd --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css @@ -0,0 +1,25 @@ +.container { + display: flex; + text-align: left; + align-items: center; + padding: 16px; + gap: 12px; + border: var(--ta-border-width-m) solid var(--ta-color-background-tertiary); + border-radius: var(--ta-border-radius-xl); + cursor: pointer; + background: transparent; +} + +.icon { + border-radius: var(--ta-border-radius-s); +} + +.name { + composes: bodySemibold from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} + +.methods { + composes: labelRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx new file mode 100644 index 000000000..4bb74acd5 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx @@ -0,0 +1,36 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ComponentProps, FC } from 'react'; +import clsx from 'clsx'; + +import type { OnrampProvider } from '../../types'; +import { Logo } from '../../../../components/logo'; +import styles from './onramp-provider-item.module.css'; + +export interface OnrampProviderItemProps extends ComponentProps<'button'> { + provider: OnrampProvider; +} + +export const OnrampProviderItem: FC = ({ provider, className, ...props }) => { + return ( + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css index 78760e714..05c81a820 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css @@ -4,51 +4,5 @@ padding: 0; display: flex; flex-direction: column; -} - -.item { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - padding: 12px 8px; - border: none; - background: none; - cursor: pointer; - border-radius: var(--ta-border-radius-m); - text-align: left; - transition: background 0.15s; -} - -.item:hover { - background: var(--ta-color-bg-secondary); -} - -.logo { - composes: labelSemibold from "../../../../styles/typography.module.css"; - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--ta-color-bg-secondary); - color: var(--ta-color-text); - flex-shrink: 0; -} - -.info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.name { - composes: bodyMedium from "../../../../styles/typography.module.css"; - color: var(--ta-color-text); -} - -.description { - composes: labelRegular from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); + gap: 8px; } diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx index 931c39491..0701d33e0 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx @@ -11,6 +11,7 @@ import type { FC } from 'react'; import { Modal } from '../../../../components/modal'; import type { OnrampProvider } from '../../types'; import styles from './onramp-provider-select.module.css'; +import { OnrampProviderItem } from '../onramp-provider-item'; export interface OnrampProviderSelectProps { open: boolean; @@ -26,22 +27,12 @@ export const OnrampProviderSelect: FC = ({ open, onCl }; return ( - !isOpen && onClose()} title="Method of purchase"> -
    + !isOpen && onClose()} title="Checkout"> +
    {providers.map((provider) => ( -
  • - -
  • + ))} -
+
); }; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx index d54cf4900..ba933ab57 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-token-selectors/onramp-token-selectors.tsx @@ -31,6 +31,7 @@ export const OnrampTokenSelectors: FC = ({
= ({ void; + presetAmounts: OnrampAmountPreset[]; /** Available payment providers */ providers: OnrampProvider[]; - /** Currently selected provider */ - selectedProvider: OnrampProvider | null; - /** Select a payment provider */ - setSelectedProvider: (provider: OnrampProvider) => void; /** Whether amount is valid and user can proceed */ canContinue: boolean; /** Current error, e.g. 'noQuotesFound' */ - error: string | null; - /** Whether a purchase is in progress (mocked) */ - isPurchasing: boolean; - /** Start the purchase (mocked) */ - onPurchase: () => void; + error?: string; /** Reset widget to initial state */ onReset: () => void; } @@ -78,18 +73,13 @@ const defaultContext: OnrampContextType = { setSelectedCurrency: () => {}, amount: '', setAmount: () => {}, - amountInputMode: 'token', + amountInputMode: 'currency', setAmountInputMode: () => {}, convertedAmount: '', presetAmounts: DEFAULT_PRESETS, - setPresetAmount: () => {}, providers: [], - selectedProvider: null, - setSelectedProvider: () => {}, canContinue: false, - error: null, - isPurchasing: false, - onPurchase: () => {}, + error: undefined, onReset: () => {}, }; @@ -123,14 +113,10 @@ export const OnrampWidgetProvider: FC = ({ ); const [amount, setAmount] = useState(''); - const [amountInputMode, setAmountInputMode] = useState('token'); + const [amountInputMode, setAmountInputMode] = useState('currency'); const [selectedProvider, setSelectedProvider] = useState(null); const [isPurchasing, setIsPurchasing] = useState(false); - const setPresetAmount = useCallback((value: number) => { - setAmount(String(value)); - }, []); - const convertedAmount = useMemo(() => { const num = parseFloat(amount); if (!amount || isNaN(num)) return ''; @@ -141,8 +127,8 @@ export const OnrampWidgetProvider: FC = ({ }, [amount, amountInputMode]); const numericAmount = parseFloat(amount); - const error = !isNaN(numericAmount) && numericAmount > ERROR_THRESHOLD ? 'noQuotesFound' : null; - const canContinue = amount !== '' && !isNaN(numericAmount) && numericAmount > 0 && error === null; + const error = !isNaN(numericAmount) && numericAmount > ERROR_THRESHOLD ? 'noQuotesFound' : undefined; + const canContinue = amount !== '' && !isNaN(numericAmount) && numericAmount > 0 && !error; const onPurchase = useCallback(() => { setIsPurchasing(true); @@ -172,7 +158,6 @@ export const OnrampWidgetProvider: FC = ({ setAmountInputMode, convertedAmount, presetAmounts: DEFAULT_PRESETS, - setPresetAmount, providers: ONRAMP_PROVIDERS, selectedProvider, setSelectedProvider, @@ -189,7 +174,6 @@ export const OnrampWidgetProvider: FC = ({ amount, amountInputMode, convertedAmount, - setPresetAmount, selectedProvider, canContinue, error, diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css index 3e48b1750..5a2d6bda7 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.module.css @@ -1,23 +1,23 @@ .widget { box-sizing: border-box; width: 100%; - max-width: 340px; + max-width: 370px; display: flex; flex-direction: column; - gap: 8px; } -.tabsRow { - display: flex; - justify-content: center; +.selectors { + margin-bottom: 64px; +} + +.input { + margin-bottom: 16px; } -.convertedLine { - composes: bodyRegular from "../../../../styles/typography.module.css"; - color: var(--ta-color-text-secondary); - text-align: center; +.converted { + margin-bottom: 64px; } -.error { - color: var(--ta-color-negative); +.presets { + margin-bottom: 16px; } diff --git a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx index 82f032422..2a41d82ef 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-widget-ui/onramp-widget-ui.tsx @@ -17,8 +17,9 @@ import { OnrampAmountPresets } from '../onramp-amount-presets'; import { OnrampTokenSelectModal } from '../onramp-token-select-modal'; import { OnrampCurrencySelectModal } from '../onramp-currency-select-modal'; import { OnrampProviderSelect } from '../onramp-provider-select'; -import { OnrampCheckout } from '../onramp-checkout'; import styles from './onramp-widget-ui.module.css'; +import { OnrampAmountReversed } from '../onramp-amount-reversed'; +import type { OnrampProvider } from '../../types'; export type OnrampWidgetRenderProps = OnrampContextType; @@ -32,79 +33,62 @@ export const OnrampWidgetUI: FC = ({ amount, setAmount, amountInputMode, - // setAmountInputMode, + setAmountInputMode, convertedAmount, presetAmounts, - setPresetAmount, providers, - selectedProvider, - setSelectedProvider, canContinue, error, - isPurchasing, - onPurchase, + onReset, }) => { const [isTokenSelectOpen, setIsTokenSelectOpen] = useState(false); const [isCurrencySelectOpen, setIsCurrencySelectOpen] = useState(false); const [isProviderSelectOpen, setIsProviderSelectOpen] = useState(false); - const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); - - const convertedSymbol = amountInputMode === 'token' ? selectedCurrency.symbol : (selectedToken?.symbol ?? ''); const handleContinue = useCallback(() => { - if (selectedProvider) { - setIsCheckoutOpen(true); - } else { - setIsProviderSelectOpen(true); - } - }, [selectedProvider]); - - const handleProviderSelected = useCallback( - (provider: typeof selectedProvider) => { - if (provider) { - setSelectedProvider(provider); - setIsCheckoutOpen(true); - } - }, - [setSelectedProvider], - ); + setIsProviderSelectOpen(true); + }, []); - const fiatAmount = amountInputMode === 'token' ? convertedAmount : amount; + const handleProviderSelected = useCallback((_provider: OnrampProvider) => { + onReset(); + }, []); return (
-
- setIsTokenSelectOpen(true)} - onToClick={() => setIsCurrencySelectOpen(true)} - /> -
+ setIsTokenSelectOpen(true)} + onToClick={() => setIsCurrencySelectOpen(true)} + /> -
- {error === 'noQuotesFound' ? ( - No quotes found - ) : ( - convertedAmount && `${convertedSymbol} ${convertedAmount}` - )} -
+ setAmountInputMode(amountInputMode === 'token' ? 'currency' : 'token')} + ticker={amountInputMode === 'token' ? undefined : selectedToken?.symbol} + symbol={amountInputMode === 'token' ? selectedCurrency.symbol : undefined} + errorMessage={error} + /> - = ({ providers={providers} onSelect={handleProviderSelected} /> - - setIsCheckoutOpen(false)} - token={selectedToken} - amount={amountInputMode === 'token' ? amount : convertedAmount} - fiatAmount={fiatAmount} - fiatSymbol={selectedCurrency.symbol} - provider={selectedProvider} - isPurchasing={isPurchasing} - onConfirm={onPurchase} - />
); }; diff --git a/packages/appkit-react/src/features/onramp/mock-data/currencies.ts b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts index 8e33d5270..be9e8c3d5 100644 --- a/packages/appkit-react/src/features/onramp/mock-data/currencies.ts +++ b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts @@ -9,12 +9,12 @@ import type { OnrampCurrency } from '../types'; export const ONRAMP_CURRENCIES: OnrampCurrency[] = [ - { code: 'EUR', name: 'Euro', symbol: '\u20AC', flag: 'https://static.moonpay.com/widget/currencies/eur.svg' }, - { code: 'USD', name: 'US Dollar', symbol: '$', flag: 'https://static.moonpay.com/widget/currencies/usd.svg' }, + { code: 'EUR', name: 'Euro', symbol: '\u20AC', logo: 'https://static.moonpay.com/widget/currencies/eur.svg' }, + { code: 'USD', name: 'US Dollar', symbol: '$', logo: 'https://static.moonpay.com/widget/currencies/usd.svg' }, { code: 'GBP', name: 'Pound Sterling', symbol: '\u00A3', - flag: 'https://static.moonpay.com/widget/currencies/gbp.svg', + logo: 'https://static.moonpay.com/widget/currencies/gbp.svg', }, ]; diff --git a/packages/appkit-react/src/features/onramp/mock-data/providers.ts b/packages/appkit-react/src/features/onramp/mock-data/providers.ts index 4ba3ce734..57aa2193d 100644 --- a/packages/appkit-react/src/features/onramp/mock-data/providers.ts +++ b/packages/appkit-react/src/features/onramp/mock-data/providers.ts @@ -12,16 +12,19 @@ export const ONRAMP_PROVIDERS: OnrampProvider[] = [ { id: 'moonpay', name: 'MoonPay', - description: 'Visa, Mastercard, Apple Pay, Google Pay, SEPA', + description: 'SEPA, PayPal, Debit Card and other options', + logo: 'https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png', }, { id: 'transak', name: 'Transak', - description: 'Visa, Mastercard, Apple Pay, Google Pay, SEPA', + description: 'Debit Card, Apple Pay, Google Pay, SEPA', + logo: 'https://cdn.meld.io/images-serviceprovider/TRANSAK/short_logo_light.png', }, { id: 'binance', name: 'Binance', - description: 'Visa, Mastercard, Apple Pay, Binance Card', + description: 'Debit Card, Apple Pay, Binance Cash Balance and other options', + logo: 'https://cdn.meld.io/images-serviceprovider/BINANCECONNECT/short_logo_light.png', }, ]; diff --git a/packages/appkit-react/src/features/onramp/types.ts b/packages/appkit-react/src/features/onramp/types.ts index 98a113865..65122d7b5 100644 --- a/packages/appkit-react/src/features/onramp/types.ts +++ b/packages/appkit-react/src/features/onramp/types.ts @@ -9,8 +9,8 @@ export interface OnrampCurrency { code: string; name: string; - symbol: string; - flag?: string; + symbol?: string; + logo?: string; } export interface OnrampProvider { @@ -21,3 +21,8 @@ export interface OnrampProvider { } export type AmountInputMode = 'token' | 'currency'; + +export interface OnrampAmountPreset { + amount: string; + label: string; +} From 3e3d2886e4a7f608b93e4a99704fe5ea7c9cc09b Mon Sep 17 00:00:00 2001 From: VK Date: Tue, 7 Apr 2026 23:00:06 +0400 Subject: [PATCH 05/67] feat(onramp): add resize for input --- .../onramp-amount-input.module.css | 15 +++- .../onramp-amount-input.tsx | 72 ++++++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css index 42121dead..5e696fc1e 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.module.css @@ -37,7 +37,7 @@ color: var(--ta-color-text-tertiary); white-space: nowrap; user-select: none; - margin-left: 8px; + margin-left: 0.2em; } .symbol { @@ -54,3 +54,16 @@ white-space: nowrap; pointer-events: none; } + +.measureRow { + display: flex; + align-items: baseline; + position: absolute; + visibility: hidden; + white-space: nowrap; + pointer-events: none; +} + +.measureText { + composes: inputXl from "../../../../styles/typography.module.css"; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx index f14ee72b4..25d241dfc 100644 --- a/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-input/onramp-amount-input.tsx @@ -6,12 +6,14 @@ * */ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import type { FC, ComponentProps } from 'react'; import clsx from 'clsx'; import styles from './onramp-amount-input.module.css'; +const MIN_FONT_SCALE = 0.5; + export interface OnrampAmountInputProps extends ComponentProps<'div'> { value: string; onValueChange: (value: string) => void; @@ -29,20 +31,65 @@ export const OnrampAmountInput: FC = ({ className, ...props }) => { + const wrapperRef = useRef(null); + const measureRowRef = useRef(null); const mirrorRef = useRef(null); const inputRef = useRef(null); const [inputWidth, setInputWidth] = useState(undefined); + const [fontScale, setFontScale] = useState(1); - useLayoutEffect(() => { - if (mirrorRef.current) { - setInputWidth(mirrorRef.current.offsetWidth + 2); + const adjustSize = useCallback(() => { + const wrapper = wrapperRef.current; + const measureRow = measureRowRef.current; + const mirror = mirrorRef.current; + + if (!wrapper || !measureRow || !mirror) return; + + const contentWidth = measureRow.offsetWidth; + const availableWidth = wrapper.clientWidth - 4; + + let scale = 1; + if (contentWidth > 0 && contentWidth > availableWidth) { + scale = Math.max(MIN_FONT_SCALE, availableWidth / contentWidth); } - }, [value, placeholder]); + + setFontScale(scale); + setInputWidth(mirror.offsetWidth * scale + 4); + }, []); + + useLayoutEffect(adjustSize, [value, placeholder, symbol, ticker, adjustSize]); + + useLayoutEffect(() => { + const wrapper = wrapperRef.current; + if (!wrapper) return; + + const observer = new ResizeObserver(adjustSize); + observer.observe(wrapper); + return () => observer.disconnect(); + }, [adjustSize]); + + const scaledInputFontSize = fontScale < 1 ? `calc(var(--ta-input-xl-size) * ${fontScale})` : undefined; + const scaledTickerFontSize = fontScale < 1 ? `calc(var(--ta-input-xl-symbol-size) * ${fontScale})` : undefined; return ( -
inputRef.current?.focus()} {...props}> -
+
inputRef.current?.focus()} + {...props} + > + + +
+ {symbol && ( + + {symbol} + + )} = ({ placeholder={placeholder} value={value} onChange={(e) => onValueChange(e.target.value)} - style={{ width: inputWidth ? `${inputWidth}px` : undefined }} + style={{ + width: inputWidth ? `${inputWidth}px` : undefined, + fontSize: scaledInputFontSize, + }} /> - {ticker && {ticker}} + {ticker && ( + + {ticker} + + )}