From 69c94dee655461760b2d8ec13221e89a26b3dad9 Mon Sep 17 00:00:00 2001 From: Minh Date: Sat, 4 Feb 2023 13:40:25 +0700 Subject: [PATCH] [feat] styling wert widget (#550) [LIQ-1235] Co-authored-by: tvminh --- .../components/BuyModal/BuyModalConfirmed.tsx | 22 ++- core/ui/src/components/BuyModal/index.tsx | 139 +++++++-------- core/ui/src/components/BuyModal/style.less | 5 + core/ui/src/components/BuyModal/useWertIo.ts | 135 ++++++++++++++ .../components/InfiniteOrderList/index.tsx | 19 +- .../ui/src/components/Payment/WertPayment.tsx | 164 ------------------ core/ui/src/components/Payment/index.ts | 1 - 7 files changed, 230 insertions(+), 255 deletions(-) create mode 100644 core/ui/src/components/BuyModal/useWertIo.ts delete mode 100644 core/ui/src/components/Payment/WertPayment.tsx diff --git a/core/ui/src/components/BuyModal/BuyModalConfirmed.tsx b/core/ui/src/components/BuyModal/BuyModalConfirmed.tsx index 9d8484ce..3963ae0c 100644 --- a/core/ui/src/components/BuyModal/BuyModalConfirmed.tsx +++ b/core/ui/src/components/BuyModal/BuyModalConfirmed.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ExplorerLinkBase } from '@liqnft/candy-shop-sdk'; -import { Blockchain, Order as OrderSchema } from '@liqnft/candy-shop-types'; +import { Blockchain, Order as OrderSchema, PaymentInfo } from '@liqnft/candy-shop-types'; import { formatDate } from 'utils/timer'; import { ExplorerLink } from 'components/ExplorerLink'; import { LiqImage } from 'components/LiqImage'; @@ -20,6 +20,7 @@ interface BuyModalConfirmedProps { candyShopEnv: Blockchain; explorerLink: ExplorerLinkBase; onClose: () => void; + paymentInfo?: PaymentInfo; } const PaymentErrorMessage: React.FC<{ error: PaymentErrorDetails }> = ({ error }) => { @@ -49,16 +50,28 @@ export const BuyModalConfirmed: React.FC = ({ shopPriceDecimals, error, candyShopEnv, - explorerLink + explorerLink, + paymentInfo }) => { const orderPrice = getPrice(shopPriceDecimalsMin, shopPriceDecimals, order.price, exchangeInfo); + const getConfirmHeader = () => { + if (error) return error.title; + if (paymentInfo?.wertConfirmInfo) return 'Transaction Pending Confirmation'; + return 'Transaction Confirmed'; + }; + return (
{error ? : } -
{error ? error.title : 'Transaction Confirmed'}
+
{getConfirmHeader()}
+ {paymentInfo?.wertConfirmInfo ? ( +
+ You will receive a mail once the transaction gets confirmed on the blockchain +
+ ) : null} {error ? (
@@ -106,7 +119,7 @@ export const BuyModalConfirmed: React.FC = ({
{error ? (
- ) : ( + ) : paymentInfo?.wertConfirmInfo ? null : ( <>
TRANSACTION HASH
@@ -114,6 +127,7 @@ export const BuyModalConfirmed: React.FC = ({
+
CONFIRMED ON
{formatDate(new Date())}
diff --git a/core/ui/src/components/BuyModal/index.tsx b/core/ui/src/components/BuyModal/index.tsx index 5d9aea26..cd1011c8 100644 --- a/core/ui/src/components/BuyModal/index.tsx +++ b/core/ui/src/components/BuyModal/index.tsx @@ -1,15 +1,9 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { - CandyShopPay, - ExplorerLinkBase, - OrderPayloadResponse, - getBaseUrl, - isCandyShopProdUrl -} from '@liqnft/candy-shop-sdk'; +import React, { useEffect, useState } from 'react'; + +import { CandyShopPay, ExplorerLinkBase, OrderPayloadResponse } from '@liqnft/candy-shop-sdk'; import { Blockchain, Order as OrderSchema, PaymentErrorName } from '@liqnft/candy-shop-types'; import { Modal } from 'components/Modal'; -import { WertPayment } from 'components/Payment'; import { PoweredByInBuyModal } from 'components/PoweredBy/PowerByInBuyModal'; import { Processing } from 'components/Processing'; import { ShopExchangeInfo, BuyModalState, PaymentErrorDetails, CreditCardPayAvailability } from 'model'; @@ -18,8 +12,9 @@ import { notification, NotificationType } from 'utils/rc-notification'; import { BuyModalConfirmed } from './BuyModalConfirmed'; import { BuyModalDetail } from './BuyModalDetail'; -import './style.less'; import { useCallback } from 'react'; +import { useWertIo } from './useWertIo'; +import './style.less'; enum ProcessingTextType { General = 'Processing purchase', @@ -86,7 +81,7 @@ export const BuyModal: React.FC = ({ }); }; - const onProcessingPay = (type: BuyModalState, error: any) => { + const onProcessingPay = (type: BuyModalState, error?: any) => { if (type === BuyModalState.PROCESSING) { setProcessingText(ProcessingTextType.CreditCard); setState(BuyModalState.PROCESSING); @@ -102,58 +97,63 @@ export const BuyModal: React.FC = ({ setPaymentError(error); return; } - if (type === BuyModalState.PAYMENT) { - setState(BuyModalState.PAYMENT); - } }; - const getEvmOrderPayload = useCallback(() => { - if (getEvmOrderPayloadCallback && walletPublicKey?.toString()) { - getEvmOrderPayloadCallback(walletPublicKey?.toString(), order) - .then((evmPayload: OrderPayloadResponse | undefined) => { - console.log('debugger: orderPayload=', evmPayload); - setEvmOrderPayload(evmPayload); - }) - .catch((error: Error) => { - console.log(`${Logger}: getEvmOrderPayloadCallback failed, error=`, error); - }); - } - }, [order, walletPublicKey, getEvmOrderPayloadCallback]); - - const isProd = useMemo(() => { - const url = getBaseUrl(candyShopEnv); - return isCandyShopProdUrl(url); - }, [candyShopEnv]); + const getEvmOrderPayload = useCallback( + (walletPublicKey: string) => { + if (getEvmOrderPayloadCallback) { + getEvmOrderPayloadCallback(walletPublicKey, order) + .then((evmPayload: OrderPayloadResponse | undefined) => { + console.log('debugger: orderPayload=', evmPayload); + setEvmOrderPayload(evmPayload); + }) + .catch((error: Error) => { + console.log(`${Logger}: getEvmOrderPayloadCallback failed, error=`, error); + }); + } + }, + [getEvmOrderPayloadCallback, order] + ); useEffect(() => { - // Only fetch when haven't retrieved the creditCardPayAvailability - if (creditCardPayAvailable === undefined) { - getEvmOrderPayload(); - CandyShopPay.checkPaymentAvailability({ - shopId: shopAddress, - tokenMint: order.tokenMint + CandyShopPay.checkPaymentAvailability({ + shopId: shopAddress, + tokenMint: order.tokenMint + }) + .then(() => { + setCreditCardPayAvailable(CreditCardPayAvailability.Supported); }) - .then(() => { - setCreditCardPayAvailable(CreditCardPayAvailability.Supported); - }) - .catch((error: Error) => { - console.log( - `${Logger}: checkPaymentAvailability failed, token= ${order.name} ${order.tokenAccount}, reason=`, - error.message - ); - if ( - PaymentErrorName.InsufficientPurchaseBalance === error.name || - PaymentErrorName.BelowMinPurchasePrice === error.name - ) { - // Only show notification when certain PaymentError from checkPaymentAvailability - handleError(error); - setCreditCardPayAvailable(CreditCardPayAvailability.Disabled); - return; - } - setCreditCardPayAvailable(CreditCardPayAvailability.Unsupported); - }); - } - }, [order.name, order.tokenAccount, order.tokenMint, shopAddress, creditCardPayAvailable, getEvmOrderPayload]); + .catch((error: Error) => { + console.log( + `${Logger}: checkPaymentAvailability failed, token= ${order.name} ${order.tokenAccount}, reason=`, + error.message + ); + if ( + PaymentErrorName.InsufficientPurchaseBalance === error.name || + PaymentErrorName.BelowMinPurchasePrice === error.name + ) { + // Only show notification when certain PaymentError from checkPaymentAvailability + handleError(error); + setCreditCardPayAvailable(CreditCardPayAvailability.Disabled); + return; + } + setCreditCardPayAvailable(CreditCardPayAvailability.Unsupported); + }); + }, [order.name, order.tokenAccount, order.tokenMint, shopAddress]); + + useEffect(() => { + if (!walletPublicKey) return; + getEvmOrderPayload(walletPublicKey); + }, [getEvmOrderPayload, walletPublicKey]); + + const { onPayWithWert, paymentInfo } = useWertIo({ + walletPublicKey, + shopAddress, + evmOrderPayload, + order, + onProcessingPay, + candyShopEnv + }); const modalWidth = state === BuyModalState.DISPLAY || state === BuyModalState.PAYMENT ? 1000 : 600; @@ -168,10 +168,6 @@ export const BuyModal: React.FC = ({ ); } - const onPaymentCallback = () => { - setState(BuyModalState.PAYMENT); - }; - return ( = ({ shopPriceDecimalsMin={shopPriceDecimalsMin} shopPriceDecimals={shopPriceDecimals} sellerUrl={sellerUrl} - onPayment={onPaymentCallback} - creditCardPayAvailable={creditCardPayAvailable} + onPayment={onPayWithWert} + creditCardPayAvailable={evmOrderPayload ? creditCardPayAvailable : CreditCardPayAvailability.Disabled} candyShopEnv={candyShopEnv} explorerLink={explorerLink} publicKey={walletPublicKey} @@ -209,20 +205,7 @@ export const BuyModal: React.FC = ({ error={paymentError} candyShopEnv={candyShopEnv} explorerLink={explorerLink} - /> - )} - - {state === BuyModalState.PAYMENT && evmOrderPayload && walletPublicKey && order && ( - )}
diff --git a/core/ui/src/components/BuyModal/style.less b/core/ui/src/components/BuyModal/style.less index 530af3f6..bd5f6d96 100644 --- a/core/ui/src/components/BuyModal/style.less +++ b/core/ui/src/components/BuyModal/style.less @@ -245,6 +245,11 @@ line-height: @candy-shop-font-line-height-xl; } + .candy-buy-modal-confirmed-description { + margin-bottom: 20px; + font-size: 12px; + } + .candy-buy-modal-confirmed-container { width: 100%; display: flex; diff --git a/core/ui/src/components/BuyModal/useWertIo.ts b/core/ui/src/components/BuyModal/useWertIo.ts new file mode 100644 index 00000000..c76f24b4 --- /dev/null +++ b/core/ui/src/components/BuyModal/useWertIo.ts @@ -0,0 +1,135 @@ +import { + OrderPayloadResponse, + CandyShopPay, + getBaseUrl, + isCandyShopProdUrl, + CreateWertPaymentParams +} from '@liqnft/candy-shop-sdk'; + +import WertWidget from '@wert-io/widget-initializer'; +import { + Blockchain, + ConfirmWertPaymentParams, + Order, + PaymentInfo, + SingleBase, + WertConfirmInfo +} from '@liqnft/candy-shop-types'; +import { BuyModalState } from 'model'; +import { useState } from 'react'; +import { handleError } from 'utils/ErrorHandler'; + +enum WertOrigin { + Sandbox = 'https://sandbox.wert.io', + Production = 'https://widget.wert.io' +} + +interface UseWertIoProps { + shopAddress: string; + onProcessingPay: (type: BuyModalState, error?: any) => void; + candyShopEnv: Blockchain; + order?: Order; + walletPublicKey?: string; + evmOrderPayload?: OrderPayloadResponse; +} + +const Logger = 'useWertIo'; + +export const useWertIo = ({ + walletPublicKey, + evmOrderPayload, + shopAddress, + candyShopEnv, + onProcessingPay, + order +}: UseWertIoProps) => { + const [paymentInfo, setPaymentInfo] = useState(); + + const onWertPaymentStatus = (data: any, uuid: string) => { + console.log(`${Logger}: onWertPaymentStatus, data=`, data); + if (data.status === 'pending' && uuid) { + // Sending the confirm info to CandyShop server + const params: ConfirmWertPaymentParams = { + shopId: shopAddress, + paymentEntityId: uuid, + paymentStatus: data + }; + CandyShopPay.confirmPayment(params) + .then((res: SingleBase) => { + console.log(`${Logger}: confirmPayment on CandyShop server success, res=`, res.result); + // Transit to Confirm UI after certain timeouts + onProcessingPay(BuyModalState.CONFIRMED); + setPaymentInfo(res.result); + }) + .catch((err: Error) => { + console.error(`${Logger}: confirmPayment on CandyShop server failed, error=`, err); + }); + } else if (data.status === 'success') { + onProcessingPay(BuyModalState.CONFIRMED); + } + }; + + const onOpenWertWidget = (uuid: string, wertConfirmInfo: WertConfirmInfo, order: Order) => { + const otherWidgetOptions = { + partner_id: wertConfirmInfo.partnerId, + container_id: 'wert-widget', + click_id: uuid, + origin: isCandyShopProdUrl(getBaseUrl(candyShopEnv)) ? WertOrigin.Production : WertOrigin.Sandbox, + width: 600, + height: 400, + commodity: wertConfirmInfo.commodity + }; + const nftOptions = { + extra: { + item_info: { + name: order.name, + seller: order.walletAddress + } + } + }; + const wertWidget = new WertWidget({ + ...wertConfirmInfo.signedData, + ...otherWidgetOptions, + ...nftOptions, + listeners: { + ['position']: (data: any) => console.log(`${Logger}: step:`, data.step), + ['payment-status']: (data: any) => onWertPaymentStatus(data, uuid), + ['close']: () => console.log(`${Logger}: close`), + ['loaded']: () => console.log(`${Logger}: loaded`) + } + }); + wertWidget.open(); + onProcessingPay(BuyModalState.PROCESSING); + }; + + const onPayWithWert = () => { + if (!walletPublicKey || !evmOrderPayload || !order) return; + const params: CreateWertPaymentParams = { + shopProgramId: order.programId, + shopId: shopAddress, + shopCreatorAddress: order.candyShopCreatorAddress, + buyerWalletAddress: walletPublicKey, + tokenAccount: order.tokenAccount, + tokenMint: order.tokenMint, + orderPayload: evmOrderPayload + }; + CandyShopPay.createPayment(params) + .then((res: SingleBase) => { + console.log(`${Logger}: createPayment success, res=`, res.result); + const wertConfirmInfo = res.result.wertConfirmInfo; + if (!wertConfirmInfo) { + console.error(`${Logger}: createPayment failed, invalid wertConfirmInfo`); + return; + } + const entityId = res.result.paymentEntityId; + onOpenWertWidget(entityId, wertConfirmInfo, order); + }) + .catch((err: Error) => { + handleError(err); + console.error(`${Logger}: createPayment failed, error=`, err); + onProcessingPay(BuyModalState.PAYMENT_ERROR); + }); + }; + + return { onPayWithWert, paymentInfo }; +}; diff --git a/core/ui/src/components/InfiniteOrderList/index.tsx b/core/ui/src/components/InfiniteOrderList/index.tsx index 52dc35ba..11a8adfe 100644 --- a/core/ui/src/components/InfiniteOrderList/index.tsx +++ b/core/ui/src/components/InfiniteOrderList/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Order } from '@liqnft/candy-shop-types'; import InfiniteScroll from 'react-infinite-scroll-component'; import { CandyShop, EthCandyShop, getCandyShopSync } from '@liqnft/candy-shop-sdk'; @@ -41,13 +41,16 @@ export const InfiniteOrderList: React.FC = ({ return store.buy(order); }; - const getEvmOrderPayloadCallback = async (buyerAddress: string, order: Order) => { - if (candyShop instanceof EthCandyShop) { - const payload = await candyShop.getNftPurchasePayload({ buyerAddress, orderUuid: order.txHash }); - return payload; - } - return undefined; - }; + const getEvmOrderPayloadCallback = useCallback( + async (buyerAddress: string, order: Order) => { + if (candyShop instanceof EthCandyShop) { + const payload = await candyShop.getNftPurchasePayload({ buyerAddress, orderUuid: order.txHash }); + return payload; + } + return undefined; + }, + [candyShop] + ); const [selectedOrder, setSelectedOrder] = useState(); const onSelectOrder = (order?: Order) => setSelectedOrder(order); diff --git a/core/ui/src/components/Payment/WertPayment.tsx b/core/ui/src/components/Payment/WertPayment.tsx deleted file mode 100644 index 1177f49e..00000000 --- a/core/ui/src/components/Payment/WertPayment.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import WertWidget from '@wert-io/widget-initializer'; -import React, { useCallback, useEffect, useState } from 'react'; -import { CandyShopPay, CreateWertPaymentParams, OrderPayloadResponse } from '@liqnft/candy-shop-sdk'; -import { ConfirmWertPaymentParams, Order, PaymentInfo, SingleBase, WertConfirmInfo } from '@liqnft/candy-shop-types'; -import { NftVerification } from 'components/Tooltip/NftVerification'; -import { Viewer } from 'components/Viewer'; -import { getPrice } from 'utils/getPrice'; -import { BuyModalState, PaymentErrorDetails, ShopExchangeInfo } from 'model'; -import { handleError } from 'utils/ErrorHandler'; - -const Logger = 'CandyShopUI/WertPayment'; - -// Refer to https://wert-io.notion.site/Getting-started-f4b5517c31364b7f8c7bdb6f94a51426#8a81ce141b4b453991fd0492a44f328c -enum WertOrigin { - Sandbox = 'https://sandbox.wert.io', - Production = 'https://widget.wert.io' -} - -interface WertPaymentProps { - shopAddress: string; - walletAddress: string; - order: Order; - exchangeInfo: ShopExchangeInfo; - shopPriceDecimalsMin: number; - shopPriceDecimals: number; - orderPayload: OrderPayloadResponse; - isProd: boolean; - onProcessingPay: (type: BuyModalState, error?: PaymentErrorDetails) => void; -} - -export const WertPayment: React.FC = ({ - shopAddress, - walletAddress, - order, - exchangeInfo, - shopPriceDecimals, - shopPriceDecimalsMin, - orderPayload, - isProd, - onProcessingPay -}) => { - const [paymentStatus, setPaymentStatus] = useState('init'); - - const handlePaymentStatusCallback = useCallback( - (data: any, uuid: string) => { - /** - * TBD UX points (remove this comment once finalize): - * 1. We should show some loading state when data.status === 'pending' in our BuyModal? Wert's widget is just a label - * 2. We can turn into BuyModalState.CONFIRMED after timeout when data.status === success (like following is doing). - * 3. Or we should consider to combine Wert Widget into our BuyModal transition? - * 4. Don't use our BuyModal transition? - */ - console.log(`${Logger}: handlePaymentStatusCallback, data=`, data); - if (data.status === 'pending' && uuid) { - // Sending the confirm info to CandyShop server - const params: ConfirmWertPaymentParams = { - shopId: shopAddress, - paymentEntityId: uuid, - paymentStatus: data - }; - CandyShopPay.confirmPayment(params) - .then((res: SingleBase) => { - console.log(`${Logger}: confirmPayment on CandyShop server success, res=`, res.result); - }) - .catch((err: Error) => { - console.error(`${Logger}: confirmPayment on CandyShop server failed, error=`, err); - }); - } else if (data.status === 'success') { - // Transit to Confirm UI after certain timeouts - setTimeout(() => { - onProcessingPay(BuyModalState.CONFIRMED); - }, 5000); - } - }, - [onProcessingPay, shopAddress] - ); - - const renderWidgetUI = useCallback( - (uuid: string, wertConfirmInfo: WertConfirmInfo) => { - setPaymentStatus('rendering'); - const otherWidgetOptions = { - partner_id: wertConfirmInfo.partnerId, - container_id: 'wert-widget', - click_id: uuid, - origin: isProd ? WertOrigin.Production : WertOrigin.Sandbox, - width: 600, - height: 400, - commodity: wertConfirmInfo.commodity - }; - const nftOptions = { - extra: { - item_info: { - name: order.name, - seller: order.walletAddress - } - } - }; - const wertWidget = new WertWidget({ - ...wertConfirmInfo.signedData, - ...otherWidgetOptions, - ...nftOptions, - listeners: { - ['position']: (data: any) => console.log(`${Logger}: step:`, data.step), - ['payment-status']: (data: any) => handlePaymentStatusCallback(data, uuid) - } - }); - wertWidget.mount(); - }, - [order, handlePaymentStatusCallback, isProd] - ); - - useEffect(() => { - // Call createPayment only when init - if (paymentStatus === 'init') { - const params: CreateWertPaymentParams = { - shopProgramId: order.programId, - shopId: shopAddress, - shopCreatorAddress: order.candyShopCreatorAddress, - buyerWalletAddress: walletAddress, - tokenAccount: order.tokenAccount, - tokenMint: order.tokenMint, - orderPayload - }; - CandyShopPay.createPayment(params) - .then((res: SingleBase) => { - console.log(`${Logger}: createPayment success, res=`, res.result); - const wertConfirmInfo = res.result.wertConfirmInfo; - if (!wertConfirmInfo) { - console.error(`${Logger}: createPayment failed, invalid wertConfirmInfo`); - return; - } - const entityId = res.result.paymentEntityId; - renderWidgetUI(entityId, wertConfirmInfo); - }) - .catch((err: Error) => { - handleError(err); - console.error(`${Logger}: createPayment failed, error=`, err); - onProcessingPay(BuyModalState.PAYMENT_ERROR); - }); - } - }, [paymentStatus, onProcessingPay, shopAddress, order, orderPayload, walletAddress, renderWidgetUI]); - - const orderPrice = getPrice(shopPriceDecimalsMin, shopPriceDecimals, order.price, exchangeInfo); - - return ( - <> -
-
-
- -
-
- {order?.name} - {order.verifiedNftCollection ? : null} -
-
PRICE
-
{orderPrice ? `${orderPrice} ${exchangeInfo.symbol}` : 'N/A'}
-
-
- -
- - ); -}; diff --git a/core/ui/src/components/Payment/index.ts b/core/ui/src/components/Payment/index.ts index 06a0a6d4..e69de29b 100644 --- a/core/ui/src/components/Payment/index.ts +++ b/core/ui/src/components/Payment/index.ts @@ -1 +0,0 @@ -export * from './WertPayment';