diff --git a/packages/checkout/sdk/src/availability/availability.test.ts b/packages/checkout/sdk/src/availability/availability.test.ts index 9f73cf7bc0..4f3d4ac73f 100644 --- a/packages/checkout/sdk/src/availability/availability.test.ts +++ b/packages/checkout/sdk/src/availability/availability.test.ts @@ -48,4 +48,43 @@ describe('availabilityService', () => { ); }); }); + + describe('checkOnRampAvailability', () => { + it('should return true when status is 2xx', async () => { + const mockResponse = {}; + mockedAxios.post.mockResolvedValueOnce(mockResponse); + const response = await availabilityService(true, false).checkOnRampAvailability(); + + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + expect(response).toEqual(true); + }); + + it('should return false when status is 403', async () => { + const mockResponse = { + status: 403, + }; + mockedAxios.post.mockRejectedValueOnce({ response: mockResponse }); + const response = await availabilityService(true, false).checkOnRampAvailability(); + + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + expect(response).toEqual(false); + }); + + it('should throw error when status is neither 204 or 403', async () => { + const mockResponse = { + status: 500, + statusText: 'error message', + }; + mockedAxios.post.mockRejectedValueOnce({ response: mockResponse }); + + await expect(availabilityService(true, false).checkOnRampAvailability()) + .rejects + .toThrow( + new CheckoutError( + 'Error fetching from api: 500 error message', + CheckoutErrorType.API_ERROR, + ), + ); + }); + }); }); diff --git a/packages/checkout/sdk/src/availability/availability.ts b/packages/checkout/sdk/src/availability/availability.ts index 12fcb4098f..104165321d 100644 --- a/packages/checkout/sdk/src/availability/availability.ts +++ b/packages/checkout/sdk/src/availability/availability.ts @@ -5,6 +5,7 @@ import { ENV_DEVELOPMENT, IMMUTABLE_API_BASE_URL } from '../env'; export type AvailabilityService = { checkDexAvailability: () => Promise + checkOnRampAvailability: () => Promise }; export const availabilityService = ( @@ -18,11 +19,11 @@ export const availabilityService = ( return IMMUTABLE_API_BASE_URL[Environment.SANDBOX]; }; - const checkDexAvailability = async (): Promise => { + const checkAvailability = async (endpoint: string): Promise => { let response; try { - response = await axios.post(`${postEndpoint()}/v1/availability/checkout/swap`); + response = await axios.post(endpoint); } catch (err: any) { // The request was made and the server responded with a status code // that falls out of the range of 2xx @@ -41,7 +42,14 @@ export const availabilityService = ( return true; }; + // eslint-disable-next-line max-len + const checkDexAvailability = async (): Promise => checkAvailability(`${postEndpoint()}/v1/availability/checkout/swap`); + + // eslint-disable-next-line max-len + const checkOnRampAvailability = async (): Promise => checkAvailability(`${postEndpoint()}/v1/availability/checkout/onramp`); + return { checkDexAvailability, + checkOnRampAvailability, }; }; diff --git a/packages/checkout/sdk/src/sdk.ts b/packages/checkout/sdk/src/sdk.ts index 7ae95d66c2..65d0759617 100644 --- a/packages/checkout/sdk/src/sdk.ts +++ b/packages/checkout/sdk/src/sdk.ts @@ -767,6 +767,14 @@ export class Checkout { return this.availability.checkDexAvailability(); } + /** + * Fetches OnRamp widget availability. + * @returns {Promise} - A promise that resolves to a boolean. + */ + public async isOnRampAvailable(): Promise { + return this.availability.checkOnRampAvailability(); + } + /** * Fetches a quote and then performs the approval and swap transaction. * @param {SwapParams} params - The parameters for the swap. diff --git a/packages/checkout/widgets-lib/src/widgets/swap/GeoblockLoader.tsx b/packages/checkout/widgets-lib/src/components/Geoblock/GeoblockLoader.tsx similarity index 90% rename from packages/checkout/widgets-lib/src/widgets/swap/GeoblockLoader.tsx rename to packages/checkout/widgets-lib/src/components/Geoblock/GeoblockLoader.tsx index dc96382a02..cd58808fbd 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/GeoblockLoader.tsx +++ b/packages/checkout/widgets-lib/src/components/Geoblock/GeoblockLoader.tsx @@ -10,12 +10,14 @@ type GeoblockLoaderParams = { widget: React.ReactNode; serviceUnavailableView: React.ReactNode; checkout: Checkout, + checkAvailability: () => Promise, }; export function GeoblockLoader({ widget, serviceUnavailableView, checkout, + checkAvailability, }: GeoblockLoaderParams) { const { t } = useTranslation(); const { showLoader, hideLoader, isLoading } = useHandover(); @@ -27,7 +29,7 @@ export function GeoblockLoader({ try { showLoader({ text: t('views.LOADING_VIEW.text') }); setRequested(true); - setAvailable(await checkout.isSwapAvailable()); + setAvailable(await checkAvailability()); hideLoader(); } catch { hideLoader(); diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx index 43922556e4..eabcc5dc3f 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx @@ -121,6 +121,13 @@ export default function AddTokensWidget({ isSwapAvailable: await checkout.isSwapAvailable(), }, }); + + addTokensDispatch({ + payload: { + type: AddTokensActions.SET_IS_ONRAMP_AVAILABLE, + isOnRampAvailable: await checkout.isOnRampAvailable(), + }, + }); })(); }, [checkout]); diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/context/AddTokensContext.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/context/AddTokensContext.tsx index 64bcaebada..ed841397d3 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/context/AddTokensContext.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/context/AddTokensContext.tsx @@ -16,6 +16,7 @@ export interface AddTokensState { selectedToken: TokenInfo | undefined; selectedAmount: string; isSwapAvailable: boolean | undefined; + isOnRampAvailable: boolean | undefined; } export const initialAddTokensState: AddTokensState = { @@ -30,6 +31,7 @@ export const initialAddTokensState: AddTokensState = { selectedToken: undefined, selectedAmount: '', isSwapAvailable: undefined, + isOnRampAvailable: undefined, }; export interface AddTokensContextState { @@ -52,7 +54,8 @@ type ActionPayload = | SetSelectedRouteData | SetSelectedToken | SetSelectedAmount - | SetIsSwapAvailable; + | SetIsSwapAvailable + | SetIsOnRampAvailable; export enum AddTokensActions { SET_ID = 'SET_ID', @@ -66,6 +69,7 @@ export enum AddTokensActions { SET_SELECTED_TOKEN = 'SET_SELECTED_TOKEN', SET_SELECTED_AMOUNT = 'SET_SELECTED_AMOUNT', SET_IS_SWAP_AVAILABLE = 'SET_IS_SWAP_AVAILABLE', + SET_IS_ONRAMP_AVAILABLE = 'SET_IS_ONRAMP_AVAILABLE', } export interface SetId { @@ -122,6 +126,10 @@ export interface SetIsSwapAvailable { type: AddTokensActions.SET_IS_SWAP_AVAILABLE; isSwapAvailable: boolean; } +export interface SetIsOnRampAvailable { + type: AddTokensActions.SET_IS_ONRAMP_AVAILABLE; + isOnRampAvailable: boolean; +} // eslint-disable-next-line @typescript-eslint/naming-convention export const AddTokensContext = createContext({ @@ -193,6 +201,11 @@ export const addTokensReducer: Reducer = ( ...state, isSwapAvailable: action.payload.isSwapAvailable, }; + case AddTokensActions.SET_IS_ONRAMP_AVAILABLE: + return { + ...state, + isOnRampAvailable: action.payload.isOnRampAvailable, + }; default: return state; } diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx index b1eabb9724..2f1bb1d26a 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx @@ -115,6 +115,7 @@ export function AddTokens({ selectedRouteData, selectedToken, isSwapAvailable, + isOnRampAvailable, } = addTokensState; const { viewDispatch } = useContext(ViewContext); @@ -488,14 +489,14 @@ export function AddTokens({ }; const shouldShowOnRampOption = useMemo(() => { - if (showOnrampOption && selectedToken) { + if (showOnrampOption && isOnRampAvailable && selectedToken) { const isAllowedToken = onRampAllowedTokens.find( (token) => token.address?.toLowerCase() === selectedToken.address?.toLowerCase(), ); return !!isAllowedToken; } return false; - }, [selectedToken, onRampAllowedTokens, showOnrampOption]); + }, [selectedToken, onRampAllowedTokens, showOnrampOption, isOnRampAvailable]); const showInitialEmptyState = !selectedToken; @@ -619,7 +620,7 @@ export function AddTokens({ /> - {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} + {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${getFormattedAmounts(selectedAmountUsd)}`} diff --git a/packages/checkout/widgets-lib/src/widgets/on-ramp/OnRampWidgetRoot.tsx b/packages/checkout/widgets-lib/src/widgets/on-ramp/OnRampWidgetRoot.tsx index 367937bc84..dfe2d14094 100644 --- a/packages/checkout/widgets-lib/src/widgets/on-ramp/OnRampWidgetRoot.tsx +++ b/packages/checkout/widgets-lib/src/widgets/on-ramp/OnRampWidgetRoot.tsx @@ -19,6 +19,9 @@ import { HandoverProvider } from '../../context/handover-context/HandoverProvide import { sendOnRampWidgetCloseEvent } from './OnRampWidgetEvents'; import i18n from '../../i18n'; import { orchestrationEvents } from '../../lib/orchestrationEvents'; +import { GeoblockLoader } from '../../components/Geoblock/GeoblockLoader'; +import { ServiceUnavailableToRegionErrorView } from '../../views/error/ServiceUnavailableToRegionErrorView'; +import { ServiceType } from '../../views/error/serviceTypes'; const OnRampWidget = React.lazy(() => import('./OnRampWidget')); @@ -90,22 +93,34 @@ export class OnRamp extends Base { - sendOnRampWidgetCloseEvent(window)} - goBackEvent={() => this.goBackEvent(window)} - showBackButton={this.parameters.showBackButton} - > - }> - this.checkout.isOnRampAvailable()} + widget={( + sendOnRampWidgetCloseEvent(window)} + goBackEvent={() => this.goBackEvent(window)} showBackButton={this.parameters.showBackButton} + > + }> + + + + )} + serviceUnavailableView={( + sendOnRampWidgetCloseEvent(window)} /> - - + )} + /> diff --git a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx index 68c4647a83..90ca345eb3 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx @@ -168,7 +168,7 @@ export function SaleContextProvider(props: { preferredCurrency, customOrderData, waitFulfillmentSettlements, - hideExcludedPaymentTypes, + hideExcludedPaymentTypes: defaultHideExcludedPaymentTypes, }, } = props; @@ -202,6 +202,9 @@ export function SaleContextProvider(props: { const [disabledPaymentTypes, setDisabledPaymentTypes] = useState< SalePaymentTypes[] >([]); + const [hideExcludedPaymentTypes, setHideExcludedPaymentTypes] = useState< + boolean + >(defaultHideExcludedPaymentTypes); const [invalidParameters, setInvalidParameters] = useState(false); @@ -233,6 +236,30 @@ export function SaleContextProvider(props: { [], ); + useEffect(() => { + if (!checkout) { + return; + } + + (async () => { + const isOnRampAvailable = await checkout.isOnRampAvailable(); + + if (!isOnRampAvailable) { + setDisabledPaymentTypes([ + ...new Set([...excludePaymentTypes, SalePaymentTypes.DEBIT, SalePaymentTypes.CREDIT]), + ]); + + setHideExcludedPaymentTypes(true); + + return; + } + + if (excludePaymentTypes?.length > 0) { + setDisabledPaymentTypes(excludePaymentTypes); + } + })(); + }, [checkout, excludePaymentTypes]); + useEffect(() => { const getUserInfo = async () => { const signer = provider?.getSigner(); @@ -370,11 +397,6 @@ export function SaleContextProvider(props: { } }, [items, collectionName, environmentId]); - useEffect(() => { - if (excludePaymentTypes?.length <= 0) return; - setDisabledPaymentTypes(excludePaymentTypes); - }, [excludePaymentTypes]); - const values = useMemo( () => ({ config, diff --git a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx index 6e4484501b..4c44bbcb3a 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx @@ -24,7 +24,7 @@ import { HandoverProvider } from '../../context/handover-context/HandoverProvide import { topUpBridgeOption, topUpOnRampOption } from './helpers'; import { sendSwapWidgetCloseEvent } from './SwapWidgetEvents'; import i18n from '../../i18n'; -import { GeoblockLoader } from './GeoblockLoader'; +import { GeoblockLoader } from '../../components/Geoblock/GeoblockLoader'; const SwapWidget = React.lazy(() => import('./SwapWidget')); @@ -123,6 +123,7 @@ export class Swap extends Base { this.checkout.isSwapAvailable()} widget={ (