From af8248dcf20f2de535f2fba9dfc68812b5d38557 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 16 Aug 2021 14:21:22 +0300 Subject: [PATCH] Payment Modal: Support Payments 2.0 (#1375) --- src/api/gramjs/apiBuilders/messages.ts | 31 ++++--- src/api/gramjs/apiBuilders/payments.ts | 2 + src/api/gramjs/methods/payments.ts | 28 +++++- src/api/types/messages.ts | 6 +- src/api/types/updates.ts | 5 ++ src/components/auth/AuthPhoneNumber.tsx | 4 +- src/components/auth/AuthQrCode.tsx | 2 +- .../auth/helpers/getSuggestedLanguage.ts | 4 +- .../helpers/renderActionMessageText.tsx | 23 ++++- .../left/settings/SettingsLanguage.tsx | 4 +- .../middle/helpers/getCurrencySign.ts | 17 ---- .../middle/message/InlineButtons.scss | 14 ++- .../middle/message/InlineButtons.tsx | 6 +- src/components/middle/message/Invoice.scss | 4 + src/components/middle/message/Invoice.tsx | 14 ++- src/components/middle/message/Message.tsx | 6 +- src/components/payment/Checkout.tsx | 32 ++++--- src/components/payment/PaymentModal.tsx | 37 +++----- src/components/payment/ReceiptModal.tsx | 11 ++- src/components/payment/Shipping.tsx | 12 ++- src/global/types.ts | 11 ++- src/modules/actions/api/bots.ts | 4 +- src/modules/actions/api/payments.ts | 85 +++++++++++------- src/modules/actions/apiUpdaters/initial.ts | 12 ++- src/modules/actions/ui/initial.ts | 2 +- src/modules/actions/ui/payments.ts | 20 ++++- src/modules/helpers/payments.ts | 87 ++++--------------- src/modules/reducers/payments.ts | 10 ++- src/modules/selectors/payments.ts | 10 ++- src/util/formatCurrency.ts | 8 ++ src/util/getReadableErrorText.ts | 45 +++++++++- src/util/langProvider.ts | 8 +- 32 files changed, 333 insertions(+), 231 deletions(-) delete mode 100644 src/components/middle/helpers/getCurrencySign.ts create mode 100644 src/util/formatCurrency.ts diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 2af1ee897..1e6bffc10 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -29,7 +29,6 @@ import { getApiChatIdFromMtpPeer } from './chats'; import { buildStickerFromDocument } from './symbols'; import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; import { interpolateArray } from '../../../util/waveform'; -import { getCurrencySign } from '../../../components/middle/helpers/getCurrencySign'; import { buildPeer } from '../gramjsBuilders'; import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers'; @@ -113,6 +112,7 @@ type UniversalMessage = ( export function buildApiMessageWithChatId(chatId: number, mtpMessage: UniversalMessage): ApiMessage { const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined; + const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined; const isChatWithSelf = !fromId && chatId === currentUserId; const isOutgoing = (mtpMessage.out && !mtpMessage.post) || (isChatWithSelf && !mtpMessage.fwdFrom); @@ -131,7 +131,8 @@ export function buildApiMessageWithChatId(chatId: number, mtpMessage: UniversalM }; } - const action = mtpMessage.action && buildAction(mtpMessage.action, fromId, Boolean(mtpMessage.post), isOutgoing); + const action = mtpMessage.action + && buildAction(mtpMessage.action, fromId, peerId, Boolean(mtpMessage.post), isOutgoing); if (action) { content.action = action; } @@ -500,13 +501,15 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { const { description: text, title, photo, test, totalAmount, currency, receiptMsgId, } = media; - const currencySign = getCurrencySign(currency); + return { text, title, photoUrl: photo && photo.url, receiptMsgId, - description: `${currencySign}${(Number(totalAmount) / 100).toFixed(2)} ${test ? 'TEST INVOICE' : ''}`, + amount: Number(totalAmount), + currency, + isTest: test, }; } @@ -567,6 +570,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef function buildAction( action: GramJs.TypeMessageAction, senderId: number | undefined, + targetPeerId: number | undefined, isChannelPost: boolean, isOutgoing: boolean, ): ApiAction | undefined { @@ -574,7 +578,9 @@ function buildAction( return undefined; } - let text = ''; + let amount: number | undefined; + let currency: string | undefined; + let text: string; const translationValues = []; let type: ApiAction['type'] = 'other'; let photo: ApiPhoto | undefined; @@ -661,10 +667,13 @@ function buildAction( translationValues.push('%action_origin%'); type = 'contactSignUp'; } else if (action instanceof GramJs.MessageActionPaymentSent) { - const currencySign = getCurrencySign(action.currency); - const amount = (Number(action.totalAmount) / 100).toFixed(2); - text = 'Notification.PaymentSent'; - translationValues.push(currencySign, amount, '%product%'); + amount = Number(action.totalAmount); + currency = action.currency; + text = 'PaymentSuccessfullyPaid'; + if (targetPeerId) { + targetUserIds.push(targetPeerId); + } + translationValues.push('%payment_amount%', '%target_user%', '%product%'); } else if (action instanceof GramJs.MessageActionGroupCall) { if (action.duration) { const mins = Math.max(Math.round(action.duration / 60), 1); @@ -691,6 +700,8 @@ function buildAction( targetUserIds, targetChatId, photo, // TODO Only used internally now, will be used for the UI in future + amount, + currency, translationValues, }; } @@ -739,7 +750,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi type = 'requestPoll'; } else if (button instanceof GramJs.KeyboardButtonBuy) { if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) { - text = 'Receipt'; + text = 'PaymentReceipt'; value = media.receiptMsgId; } type = 'buy'; diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 431ec3fc3..b2156432a 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -63,6 +63,7 @@ export function buildReceipt(receipt: GramJs.payments.PaymentReceipt) { export function buildPaymentForm(form: GramJs.payments.PaymentForm) { const { + formId, canSaveCredentials, passwordMissing, providerId, @@ -94,6 +95,7 @@ export function buildPaymentForm(form: GramJs.payments.PaymentForm) { return { canSaveCredentials, passwordMissing, + formId: String(formId), providerId, nativeProvider, savedInfo, diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 5024c0012..7246e4626 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -1,13 +1,17 @@ +import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import { invokeRequest } from './client'; -import { buildShippingInfo } from '../gramjsBuilders'; +import { buildInputPeer, buildShippingInfo } from '../gramjsBuilders'; import { buildShippingOptions, buildPaymentForm, buildReceipt } from '../apiBuilders/payments'; +import { ApiChat } from '../../types'; export async function validateRequestedInfo({ + chat, messageId, requestInfo, shouldSave, }: { + chat: ApiChat; messageId: number; requestInfo: GramJs.TypePaymentRequestedInfo; shouldSave?: boolean; @@ -16,6 +20,7 @@ export async function validateRequestedInfo({ shippingOptions: any; } | undefined> { const result = await invokeRequest(new GramJs.payments.ValidateRequestedInfo({ + peer: buildInputPeer(chat.id, chat.accessHash), msgId: messageId, save: shouldSave || undefined, info: buildShippingInfo(requestInfo), @@ -23,10 +28,12 @@ export async function validateRequestedInfo({ if (!result) { return undefined; } + const { id, shippingOptions } = result; if (!id) { return undefined; } + return { id, shippingOptions: buildShippingOptions(shippingOptions), @@ -34,17 +41,23 @@ export async function validateRequestedInfo({ } export function sendPaymentForm({ + chat, messageId, + formId, requestedInfoId, shippingOptionId, credentials, }: { + chat: ApiChat; messageId: number; + formId: string; credentials: any; requestedInfoId?: string; shippingOptionId?: string; }) { return invokeRequest(new GramJs.payments.SendPaymentForm({ + formId: BigInt(formId), + peer: buildInputPeer(chat.id, chat.accessHash), msgId: messageId, requestedInfoId, shippingOptionId, @@ -56,13 +69,16 @@ export function sendPaymentForm({ } export async function getPaymentForm({ - messageId, + chat, messageId, }: { + chat: ApiChat; messageId: number; }) { const result = await invokeRequest(new GramJs.payments.GetPaymentForm({ + peer: buildInputPeer(chat.id, chat.accessHash), msgId: messageId, })); + if (!result) { return undefined; } @@ -70,10 +86,14 @@ export async function getPaymentForm({ return buildPaymentForm(result); } -export async function getReceipt(msgId: number) { - const result = await invokeRequest(new GramJs.payments.GetPaymentReceipt({ msgId })); +export async function getReceipt(chat: ApiChat, msgId: number) { + const result = await invokeRequest(new GramJs.payments.GetPaymentReceipt({ + peer: buildInputPeer(chat.id, chat.accessHash), + msgId, + })); if (!result) { return undefined; } + return buildReceipt(result); } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 6882a4cfc..69937a5ab 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -131,8 +131,10 @@ export interface ApiInvoice { text: string; title: string; photoUrl?: string; - description?: string; + amount: number; + currency: string; receiptMsgId?: number; + isTest?: boolean; } export type ApiNewPoll = { @@ -150,6 +152,8 @@ export interface ApiAction { targetChatId?: number; type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'other'; photo?: ApiPhoto; + amount?: number; + currency?: string; translationValues: string[]; } diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index e53becab2..03744ebd9 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -318,6 +318,11 @@ export type ApiError = { textParams?: Record; }; +export type ApiFieldError = { + field: string; + message: string; +}; + export type ApiInviteInfo = { title: string; hash: string; diff --git a/src/components/auth/AuthPhoneNumber.tsx b/src/components/auth/AuthPhoneNumber.tsx index 6bf5a9635..c8b89cf72 100644 --- a/src/components/auth/AuthPhoneNumber.tsx +++ b/src/components/auth/AuthPhoneNumber.tsx @@ -117,7 +117,7 @@ const AuthPhoneNumber: FC = ({ const handleLangChange = useCallback(() => { markIsLoading(); - setLanguage(suggestedLanguage!, () => { + void setLanguage(suggestedLanguage, () => { unmarkIsLoading(); setSettingOption({ language: suggestedLanguage }); @@ -153,7 +153,7 @@ const AuthPhoneNumber: FC = ({ if (!isPreloadInitiated) { isPreloadInitiated = true; preloadFonts(); - preloadImage(monkeyPath); + void preloadImage(monkeyPath); } const { value, selectionStart, selectionEnd } = e.target; diff --git a/src/components/auth/AuthQrCode.tsx b/src/components/auth/AuthQrCode.tsx index 8ce6729ff..632ea4d54 100644 --- a/src/components/auth/AuthQrCode.tsx +++ b/src/components/auth/AuthQrCode.tsx @@ -64,7 +64,7 @@ const AuthCode: FC = ({ const handleLangChange = useCallback(() => { markIsLoading(); - setLanguage(suggestedLanguage!, () => { + void setLanguage(suggestedLanguage, () => { unmarkIsLoading(); setSettingOption({ language: suggestedLanguage }); diff --git a/src/components/auth/helpers/getSuggestedLanguage.ts b/src/components/auth/helpers/getSuggestedLanguage.ts index 084413a0d..66a0bc327 100644 --- a/src/components/auth/helpers/getSuggestedLanguage.ts +++ b/src/components/auth/helpers/getSuggestedLanguage.ts @@ -1,3 +1,5 @@ +import { LangCode } from '../../../types'; + export function getSuggestedLanguage() { let suggestedLanguage = navigator.language; @@ -5,5 +7,5 @@ export function getSuggestedLanguage() { suggestedLanguage = suggestedLanguage.substr(0, 2); } - return suggestedLanguage; + return suggestedLanguage as LangCode; } diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 4949c64c2..00f1a20be 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -10,6 +10,7 @@ import { isChat, } from '../../../modules/helpers'; import trimText from '../../../util/trimText'; +import { formatCurrency } from '../../../util/formatCurrency'; import { TextPart } from './renderMessageText'; import renderText from './renderText'; @@ -37,16 +38,30 @@ export function renderActionMessageText( if (!message.content.action) { return []; } - const { text, translationValues } = message.content.action; + const { + text, translationValues, amount, currency, + } = message.content.action; const content: TextPart[] = []; const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 32 }; const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage ? 'Message.PinnedGenericMessage' : text; - let unprocessed: string; - let processed = processPlaceholder( - lang(translationKey, translationValues && translationValues.length ? translationValues : undefined), + let unprocessed = lang(translationKey, translationValues && translationValues.length ? translationValues : undefined); + let processed: TextPart[]; + + if (unprocessed.includes('%payment_amount%')) { + processed = processPlaceholder( + unprocessed, + '%payment_amount%', + formatCurrency(amount!, currency, lang.code), + ); + unprocessed = processed.pop() as string; + content.push(...processed); + } + + processed = processPlaceholder( + unprocessed, '%action_origin%', actionOrigin ? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP diff --git a/src/components/left/settings/SettingsLanguage.tsx b/src/components/left/settings/SettingsLanguage.tsx index 5a39ac66b..9ab4a682b 100644 --- a/src/components/left/settings/SettingsLanguage.tsx +++ b/src/components/left/settings/SettingsLanguage.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { ISettings, SettingsScreens } from '../../../types'; +import { ISettings, LangCode, SettingsScreens } from '../../../types'; import { ApiLanguage } from '../../../api/types'; import { setLanguage } from '../../../util/langProvider'; @@ -46,7 +46,7 @@ const SettingsLanguage: FC = ({ setSelectedLanguage(langCode); markIsLoading(); - setLanguage(langCode, () => { + void setLanguage(langCode as LangCode, () => { unmarkIsLoading(); setSettingOption({ language: langCode }); diff --git a/src/components/middle/helpers/getCurrencySign.ts b/src/components/middle/helpers/getCurrencySign.ts deleted file mode 100644 index d55a1cd19..000000000 --- a/src/components/middle/helpers/getCurrencySign.ts +++ /dev/null @@ -1,17 +0,0 @@ -const CURRENCIES: Record = { - USD: '$', - EUR: '€', - GBP: '£', - JPY: '¥', - RUB: '₽', - UAH: '₴', - INR: '₹', - AED: 'د.إ', -}; - -export function getCurrencySign(currency: string | undefined): string { - if (!currency) { - return ''; - } - return CURRENCIES[currency] || ''; -} diff --git a/src/components/middle/message/InlineButtons.scss b/src/components/middle/message/InlineButtons.scss index 4320176c9..01aec7f5e 100644 --- a/src/components/middle/message/InlineButtons.scss +++ b/src/components/middle/message/InlineButtons.scss @@ -48,12 +48,18 @@ } i { - font-size: 0.75rem; + font-size: .875rem; position: absolute; - right: 0.125rem; - top: 0.125rem; + right: .1875rem; + top: .1875rem; display: block; - transform: rotate(-45deg); + + &.icon-arrow-right { + font-size: .75rem; + top: .125rem; + right: .125rem; + transform: rotate(-45deg); + } } } diff --git a/src/components/middle/message/InlineButtons.tsx b/src/components/middle/message/InlineButtons.tsx index c461b7671..83c313e14 100644 --- a/src/components/middle/message/InlineButtons.tsx +++ b/src/components/middle/message/InlineButtons.tsx @@ -4,6 +4,7 @@ import { ApiKeyboardButton, ApiMessage } from '../../../api/types'; import { RE_TME_LINK } from '../../../config'; import renderText from '../../common/helpers/renderText'; +import useLang from '../../../hooks/useLang'; import Button from '../../ui/Button'; @@ -15,6 +16,8 @@ type OwnProps = { }; const InlineButtons: FC = ({ message, onClick }) => { + const lang = useLang(); + return (
{message.inlineButtons!.map((row) => ( @@ -26,7 +29,8 @@ const InlineButtons: FC = ({ message, onClick }) => { disabled={button.type === 'NOT_SUPPORTED'} onClick={() => onClick({ button })} > - {renderText(button.text)} + {renderText(lang(button.text))} + {button.type === 'buy' && } {button.type === 'url' && !button.value!.match(RE_TME_LINK) && } ))} diff --git a/src/components/middle/message/Invoice.scss b/src/components/middle/message/Invoice.scss index 2688232b4..087b9e8ae 100644 --- a/src/components/middle/message/Invoice.scss +++ b/src/components/middle/message/Invoice.scss @@ -26,6 +26,10 @@ border-radius: var(--border-radius-messages-small); color: var(--color-text); font-weight: 500; + + span { + margin-left: .5rem; + } } } diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index a50fe7ab8..ffa4919eb 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -3,7 +3,9 @@ import React, { FC, memo } from '../../../lib/teact/teact'; import { ApiMessage } from '../../../api/types'; import { getMessageInvoice } from '../../../modules/helpers'; +import { formatCurrency } from '../../../util/formatCurrency'; import renderText from '../../common/helpers/renderText'; +import useLang from '../../../hooks/useLang'; import './Invoice.scss'; @@ -14,12 +16,15 @@ type OwnProps = { const Invoice: FC = ({ message, }) => { + const lang = useLang(); const invoice = getMessageInvoice(message); const { title, text, - description, + amount, + currency, + isTest, photoUrl, } = invoice!; @@ -41,9 +46,10 @@ const Invoice: FC = ({ alt="" /> )} - {description && ( -

{renderText(description, ['emoji', 'br'])}

- )} +

+ {formatCurrency(amount, currency, lang.code)} + {isTest && {lang('PaymentTestInvoice')}} +

); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 4b31a4aa0..fb61ea639 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -716,11 +716,7 @@ const Message: FC = ({ onCancelMediaTransfer={handleCancelUpload} /> )} - {invoice && ( - - )} + {invoice && } ); } diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 978b31a22..1b215fd02 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -2,14 +2,16 @@ import React, { FC, memo, } from '../../lib/teact/teact'; -import { Price } from '../../types'; +import { LangCode, Price } from '../../types'; + +import { formatCurrency } from '../../util/formatCurrency'; +import useLang from '../../hooks/useLang'; import './Checkout.scss'; export type OwnProps = { invoiceContent?: { title?: string; - description?: string; text?: string; photoUrl?: string; }; @@ -35,8 +37,9 @@ const Checkout: FC = ({ currency, totalPrice, }) => { - // eslint-disable-next-line no-null/no-null - const { photoUrl, title, text } = (invoiceContent || {}); + const lang = useLang(); + + const { photoUrl, title, text } = invoiceContent || {}; const { paymentMethod, paymentProvider, @@ -45,26 +48,25 @@ const Checkout: FC = ({ phone, shippingMethod, } = (checkoutInfo || {}); + return (
- { photoUrl && ( - - )} + {photoUrl && }
-
{ title }
-

{ text }

+
{title}
+

{text}

{ prices && prices.map((item) => ( - renderPaymentItem(item.label, item.amount, currency, false) + renderPaymentItem(lang.code, item.label, item.amount, currency) )) } { shippingPrices && shippingPrices.map((item) => ( - renderPaymentItem(item.label, item.amount, currency, false) + renderPaymentItem(lang.code, item.label, item.amount, currency) )) } { totalPrice !== undefined && ( - renderPaymentItem('Total', totalPrice, currency, true) + renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, currency, true) ) }
@@ -79,14 +81,16 @@ const Checkout: FC = ({ ); }; -function renderPaymentItem(title: string, value: number, currency?: string, main = false) { +function renderPaymentItem( + langCode: LangCode | undefined, title: string, value: number, currency?: string, main = false, +) { return (
{ title }
- { `${currency || ''} ${(value / 100).toFixed(2)}` } + {formatCurrency(value, currency, langCode)}
); diff --git a/src/components/payment/PaymentModal.tsx b/src/components/payment/PaymentModal.tsx index 5f66ca86f..45e0fdda7 100644 --- a/src/components/payment/PaymentModal.tsx +++ b/src/components/payment/PaymentModal.tsx @@ -5,12 +5,10 @@ import { withGlobal } from '../../lib/teact/teactn'; import { GlobalActions, GlobalState } from '../../global/types'; import { PaymentStep, ShippingOption, Price } from '../../types'; -import { ApiError, ApiInviteInfo } from '../../api/types'; import { pick } from '../../util/iteratees'; -import { getCurrencySign } from '../middle/helpers/getCurrencySign'; +import { formatCurrency } from '../../util/formatCurrency'; import { detectCardTypeText } from '../common/helpers/detectCardType'; -import { getShippingErrors } from '../../modules/helpers/payments'; import usePaymentReducer, { FormState } from '../../hooks/reducers/usePaymentReducer'; import useLang from '../../hooks/useLang'; @@ -46,7 +44,6 @@ type StateProps = { needCardholderName?: boolean; needCountry?: boolean; needZip?: boolean; - globalDialogs?: (ApiError | ApiInviteInfo)[]; }; type GlobalStateProps = Pick = ({ needCountry, needZip, error, - globalDialogs, validateRequestedInfo, sendPaymentForm, setPaymentStep, @@ -87,36 +83,26 @@ const Invoice: FC = ({ clearPaymentError, }) => { const [paymentState, paymentDispatch] = usePaymentReducer(); - const currencySign = getCurrencySign(currency); const [isLoading, setIsLoading] = useState(false); const lang = useLang(); useEffect(() => { - if (step || error || globalDialogs) { + if (step || error) { setIsLoading(false); } - }, [step, error, globalDialogs]); + }, [step, error]); useEffect(() => { if (error && error.field) { paymentDispatch({ type: 'setFormErrors', payload: { - [error.field]: error.fieldError, + [error.field]: error.message, }, }); return; } - if (globalDialogs && globalDialogs.length) { - const errors = getShippingErrors(globalDialogs); - paymentDispatch({ - type: 'setFormErrors', - payload: { - ...errors, - }, - }); - } - }, [error, globalDialogs, paymentDispatch]); + }, [error, paymentDispatch]); useEffect(() => { if (savedInfo) { @@ -178,8 +164,8 @@ const Invoice: FC = ({ ); } - function renderModalContent(cuurentStep: PaymentStep) { - switch (cuurentStep) { + function renderModalContent(currentStep: PaymentStep) { + switch (currentStep) { case PaymentStep.ShippingInfo: return ( = ({ state={paymentState} dispatch={paymentDispatch} shippingOptions={shippingOptions || []} - currency={currencySign} + currency={currency} /> ); case PaymentStep.PaymentInfo: @@ -221,7 +207,7 @@ const Invoice: FC = ({ totalPrice={totalPrice} invoiceContent={invoiceContent} checkoutInfo={checkoutInfo} - currency={currencySign} + currency={currency} /> ); default: @@ -287,11 +273,11 @@ const Invoice: FC = ({ const buttonText = useMemo(() => { switch (step) { case PaymentStep.Checkout: - return lang('Checkout.PayPrice', `${currencySign}${(totalPrice / 100).toFixed(2)}`); + return lang('Checkout.PayPrice', formatCurrency(totalPrice, currency, lang.code)); default: return lang('Next'); } - }, [step, lang, currencySign, totalPrice]); + }, [step, lang, currency, totalPrice]); if (isProviderError) { return ( @@ -412,7 +398,6 @@ export default memo(withGlobal( needCountry, needZip, error, - globalDialogs: global.dialogs, }; }, (setGlobal, actions): DispatchProps => { diff --git a/src/components/payment/ReceiptModal.tsx b/src/components/payment/ReceiptModal.tsx index 73424224f..ff7153e8d 100644 --- a/src/components/payment/ReceiptModal.tsx +++ b/src/components/payment/ReceiptModal.tsx @@ -4,10 +4,9 @@ import React, { import { withGlobal } from '../../lib/teact/teactn'; import { Price } from '../../types'; -import { ApiShippingAddress } from '../../api/types/payments'; +import { ApiShippingAddress } from '../../api/types'; import useLang from '../../hooks/useLang'; -import { getCurrencySign } from '../middle/helpers/getCurrencySign'; import Checkout from './Checkout'; import Modal from '../ui/Modal'; @@ -52,10 +51,10 @@ const ReceiptModal: FC = ({ shippingMethod, }) => { const lang = useLang(); - const currencySign = getCurrencySign(currency); const checkoutInfo = useMemo(() => { return getCheckoutInfo(credentialsTitle, info, shippingMethod); }, [info, shippingMethod, credentialsTitle]); + return ( = ({ title, }} checkoutInfo={checkoutInfo} - currency={currencySign} + currency={currency} />
@@ -100,7 +99,7 @@ export default memo(withGlobal( const { receipt } = global.payment; const { currency, - prices: mapedPrices, + prices, info, totalAmount, credentialsTitle, @@ -113,7 +112,7 @@ export default memo(withGlobal( return { currency, - prices: mapedPrices, + prices, info, totalAmount, credentialsTitle, diff --git a/src/components/payment/Shipping.tsx b/src/components/payment/Shipping.tsx index e1477f8a7..345b5c467 100644 --- a/src/components/payment/Shipping.tsx +++ b/src/components/payment/Shipping.tsx @@ -2,9 +2,11 @@ import React, { FC, useCallback, memo, useMemo, useEffect, } from '../../lib/teact/teact'; -import { ShippingOption } from '../../types/index'; +import { ShippingOption } from '../../types'; +import { formatCurrency } from '../../util/formatCurrency'; import { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer'; +import useLang from '../../hooks/useLang'; import RadioGroup from '../ui/RadioGroup'; @@ -13,7 +15,7 @@ import './Shipping.scss'; export type OwnProps = { state: FormState; shippingOptions: ShippingOption[]; - currency: string; + currency?: string; dispatch: FormEditDispatch; }; @@ -23,6 +25,8 @@ const Shipping: FC = ({ currency, dispatch, }) => { + const lang = useLang(); + useEffect(() => { if (!shippingOptions || state.shipping) { return; @@ -36,9 +40,9 @@ const Shipping: FC = ({ const options = useMemo(() => (shippingOptions.map(({ id: value, title: label, amount }) => ({ label, - subLabel: `${currency} ${String(amount / 100)}`, + subLabel: formatCurrency(amount, currency, lang.code), value, - }))), [shippingOptions, currency]); + }))), [shippingOptions, currency, lang.code]); return (
diff --git a/src/global/types.ts b/src/global/types.ts index c2994b1db..84c11bb4c 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -19,6 +19,7 @@ import { ApiSession, ApiNewPoll, ApiInviteInfo, + ApiFieldError, } from '../api/types'; import { FocusDirection, @@ -344,18 +345,22 @@ export type GlobalState = { }; payment: { + chatId?: number; messageId?: number; step?: PaymentStep; shippingOptions?: ShippingOption[]; formId?: string; + requestId?: string; savedInfo?: ApiPaymentSavedInfo; canSaveCredentials?: boolean; invoice?: Invoice; invoiceContent?: { title?: string; text?: string; - description?: string; photoUrl?: string; + amount?: number; + currency?: string; + isTest?: boolean; }; nativeProvider?: string; providerId?: number; @@ -377,7 +382,7 @@ export type GlobalState = { receipt?: Receipt; error?: { field?: string; - fieldError?: string; + message?: string; description: string; }; isPaymentModalOpen?: boolean; @@ -501,7 +506,7 @@ export type ActionTypes = ( 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' | // payment - 'openPaymentModal' | 'closePaymentModal' | + 'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' | 'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' | 'sendCredentialsInfo' | 'setInvoiceMessageInfo' | 'clearPaymentError' | 'clearReceipt' ); diff --git a/src/modules/actions/api/bots.ts b/src/modules/actions/api/bots.ts index 2c281a8ce..8e3ac7345 100644 --- a/src/modules/actions/api/bots.ts +++ b/src/modules/actions/api/bots.ts @@ -55,9 +55,9 @@ addReducer('clickInlineButton', (global, actions, payload) => { if (value) { actions.getReceipt({ receiptMessageId: value, chatId: chat.id, messageId }); } else { - actions.getPaymentForm({ messageId }); + actions.getPaymentForm({ chat, messageId }); actions.setInvoiceMessageInfo(selectChatMessage(global, chat.id, messageId)); - actions.openPaymentModal({ messageId }); + actions.openPaymentModal({ chatId: chat.id, messageId }); } break; } diff --git a/src/modules/actions/api/payments.ts b/src/modules/actions/api/payments.ts index af9110856..b57f76bf7 100644 --- a/src/modules/actions/api/payments.ts +++ b/src/modules/actions/api/payments.ts @@ -1,16 +1,20 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; -import { PaymentStep } from '../../../types/index'; -import { callApi } from '../../../api/gramjs'; +import { PaymentStep } from '../../../types'; +import { ApiChat } from '../../../api/types'; + import { selectPaymentMessageId, selectPaymentRequestId, selectProviderPublishableKey, selectStripeCredentials, selectChatMessage, + selectPaymentChatId, + selectChat, + selectPaymentFormId, } from '../../selectors'; - -import { getStripeError } from '../../helpers/payments'; +import { callApi } from '../../../api/gramjs'; +import { getStripeError } from '../../helpers'; import { buildQueryString } from '../../../util/requestQuery'; import { @@ -27,22 +31,28 @@ import { addReducer('validateRequestedInfo', (global, actions, payload) => { const { requestInfo, saveInfo } = payload; + const chatId = selectPaymentChatId(global); + const chat = chatId && selectChat(global, chatId); const messageId = selectPaymentMessageId(global); - if (!messageId) { + if (!chat || !messageId) { return; } - validateRequestedInfo(messageId, requestInfo, saveInfo); + void validateRequestedInfo(chat, messageId, requestInfo, saveInfo); }); -async function validateRequestedInfo(messageId: number, requestInfo: any, shouldSave?: true) { - const result = await callApi('validateRequestedInfo', { messageId, requestInfo, shouldSave }); +async function validateRequestedInfo(chat: ApiChat, messageId: number, requestInfo: any, shouldSave?: true) { + const result = await callApi('validateRequestedInfo', { + chat, messageId, requestInfo, shouldSave, + }); if (!result) { return; } + const { id, shippingOptions } = result; if (!id) { return; } + let global = setRequestInfoId(getGlobal(), id); if (shippingOptions) { global = updateShippingOptions(global, shippingOptions); @@ -54,16 +64,16 @@ async function validateRequestedInfo(messageId: number, requestInfo: any, should } addReducer('getPaymentForm', (global, actions, payload) => { - const { messageId } = payload; - if (!messageId) { + const { chat, messageId } = payload; + if (!chat || !messageId) { return; } - getPaymentForm(messageId); + void getPaymentForm(chat, messageId); }); -async function getPaymentForm(messageId: number) { - const result = await callApi('getPaymentForm', { messageId }); +async function getPaymentForm(chat: ApiChat, messageId: number) { + const result = await callApi('getPaymentForm', { chat, messageId }); if (!result) { return; } @@ -82,19 +92,22 @@ async function getPaymentForm(messageId: number) { addReducer('getReceipt', (global, actions, payload) => { const { receiptMessageId, chatId, messageId } = payload; - if (!messageId || !receiptMessageId || !chatId) { + const chat = chatId && selectChat(global, chatId); + if (!messageId || !receiptMessageId || !chat) { return; } - getReceipt(messageId, receiptMessageId, chatId); + + void getReceipt(chat, messageId, receiptMessageId); }); -async function getReceipt(messageId: number, receiptMessageId: number, chatId: number) { - const result = await callApi('getReceipt', receiptMessageId); +async function getReceipt(chat: ApiChat, messageId: number, receiptMessageId: number) { + const result = await callApi('getReceipt', chat, receiptMessageId); if (!result) { return; } + let global = getGlobal(); - const message = selectChatMessage(global, chatId, messageId); + const message = selectChatMessage(global, chat.id, messageId); global = setReceipt(global, result, message); setGlobal(global); } @@ -126,34 +139,40 @@ addReducer('sendCredentialsInfo', (global, actions, payload) => { } const { credentials } = payload; const { data } = credentials; - sendStipeCredentials(data, publishableKey); + void sendStripeCredentials(data, publishableKey); }); addReducer('sendPaymentForm', (global, actions, payload) => { const { shippingOptionId, saveCredentials } = payload; + const chatId = selectPaymentChatId(global); + const chat = chatId && selectChat(global, chatId); const messageId = selectPaymentMessageId(global); + const formId = selectPaymentFormId(global); const requestInfoId = selectPaymentRequestId(global); const publishableKey = selectProviderPublishableKey(global); const stripeCredentials = selectStripeCredentials(global); - if (!messageId || !publishableKey) { + if (!chat || !messageId || !publishableKey || !formId) { return; } - sendPaymentForm(messageId, { + + void sendPaymentForm(chat, messageId, formId, { save: saveCredentials, data: stripeCredentials, }, requestInfoId, shippingOptionId); }); -async function sendStipeCredentials(data: { - cardNumber: string; - cardholder?: string; - expiryMonth: string; - expiryYear: string; - cvv: string; - country: string; - zip: string; -}, -publishableKey: string) { +async function sendStripeCredentials( + data: { + cardNumber: string; + cardholder?: string; + expiryMonth: string; + expiryYear: string; + cvv: string; + country: string; + zip: string; + }, + publishableKey: string, +) { const query = buildQueryString({ 'card[number]': data.cardNumber, 'card[exp_month]': data.expiryMonth, @@ -195,13 +214,15 @@ publishableKey: string) { } async function sendPaymentForm( + chat: ApiChat, messageId: number, + formId: string, credentials: any, requestedInfoId?: string, shippingOptionId?: string, ) { const result = await callApi('sendPaymentForm', { - messageId, credentials, requestedInfoId, shippingOptionId, + chat, messageId, formId, credentials, requestedInfoId, shippingOptionId, }); if (result) { const global = clearPayment(getGlobal()); diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index 8b40cd6b1..f2e53147a 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -18,6 +18,7 @@ import { updateUser } from '../../reducers'; import { setLanguage } from '../../../util/langProvider'; import { selectNotifySettings } from '../../selectors'; import { forceWebsync } from '../../../util/websync'; +import { getShippingError } from '../../../util/getReadableErrorText'; addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { if (DEBUG) { @@ -61,7 +62,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { actions.signOut(); } - if (actions.showDialog) { + const paymentShippingError = getShippingError(update.error); + if (paymentShippingError) { + actions.addPaymentError({ error: paymentShippingError }); + } else if (actions.showDialog) { actions.showDialog({ data: { ...update.error, hasErrorKey: true } }); } @@ -71,8 +75,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { function onUpdateApiReady(global: GlobalState) { const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global); - if (hasWebNotifications && hasPushNotifications) subscribe(); - setLanguage(global.settings.byKey.language); + if (hasWebNotifications && hasPushNotifications) { + void subscribe(); + } + void setLanguage(global.settings.byKey.language); } function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) { diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index 9ac170912..a8adc8496 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -17,7 +17,7 @@ addReducer('init', (global) => { const { animationLevel, messageTextSize, language } = global.settings.byKey; const theme = selectTheme(global); - setLanguage(language, undefined, true); + void setLanguage(language, undefined, true); document.documentElement.style.setProperty( '--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`, diff --git a/src/modules/actions/ui/payments.ts b/src/modules/actions/ui/payments.ts index 12565696d..73859dc8b 100644 --- a/src/modules/actions/ui/payments.ts +++ b/src/modules/actions/ui/payments.ts @@ -1,14 +1,14 @@ import { addReducer } from '../../../lib/teact/teactn'; -import { - clearPayment, closeInvoice, -} from '../../reducers'; + +import { clearPayment, closeInvoice } from '../../reducers'; addReducer('openPaymentModal', (global, actions, payload) => { - const { messageId } = payload; + const { chatId, messageId } = payload; return { ...global, payment: { ...global.payment, + chatId, messageId, isPaymentModalOpen: true, }, @@ -19,3 +19,15 @@ addReducer('closePaymentModal', (global) => { const newGlobal = clearPayment(global); return closeInvoice(newGlobal); }); + +addReducer('addPaymentError', (global, actions, payload) => { + const { error } = payload!; + + return { + ...global, + payment: { + ...global.payment, + error, + }, + }; +}); diff --git a/src/modules/helpers/payments.ts b/src/modules/helpers/payments.ts index 1e28de258..e980d3f50 100644 --- a/src/modules/helpers/payments.ts +++ b/src/modules/helpers/payments.ts @@ -1,41 +1,41 @@ -import { ApiError, ApiInviteInfo } from '../../api/types'; +import { ApiFieldError } from '../../api/types'; -const STRIPE_ERRORS: Record> = { +const STRIPE_ERRORS: Record = { missing_payment_information: { field: 'cardNumber', - fieldError: 'Incorrect card number', + message: 'Incorrect card number', }, invalid_number: { field: 'cardNumber', - fieldError: 'Incorrect card number', + message: 'Incorrect card number', }, number: { field: 'cardNumber', - fieldError: 'Incorrect card number', + message: 'Incorrect card number', }, exp_year: { field: 'expiry', - fieldError: 'Incorrect year', + message: 'Incorrect year', }, exp_month: { field: 'expiry', - fieldError: 'Incorrect month', + message: 'Incorrect month', }, invalid_expiry_year: { field: 'expiry', - fieldError: 'Incorrect year', + message: 'Incorrect year', }, invalid_expiry_month: { field: 'expiry', - fieldError: 'Incorrect month', + message: 'Incorrect month', }, cvc: { field: 'cvv', - fieldError: 'Incorrect CVV', + message: 'Incorrect CVV', }, invalid_cvc: { field: 'cvv', - fieldError: 'Incorrect CVV', + message: 'Incorrect CVV', }, }; @@ -44,65 +44,8 @@ export function getStripeError(error: { message: string; param?: string; }) { - const { message, code, param } = error; - const { field, fieldError, description } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code]; - return { - field, - fieldError, - description: description || message, - }; -} - -const SHIPPING_ERRORS: Record> = { - ADDRESS_STREET_LINE1_INVALID: { - field: 'streetLine1', - fieldError: 'Incorrect street address', - }, - ADDRESS_STREET_LINE2_INVALID: { - field: 'streetLine2', - fieldError: 'Incorrect street address', - }, - ADDRESS_CITY_INVALID: { - field: 'city', - fieldError: 'Incorrect city', - }, - ADDRESS_COUNTRY_INVALID: { - field: 'countryIso2', - fieldError: 'Incorrect country', - }, - ADDRESS_POSTCODE_INVALID: { - field: 'postCode', - fieldError: 'Incorrect post code', - }, - ADDRESS_STATE_INVALID: { - field: 'state', - fieldError: 'Incorrect state', - }, - REQ_INFO_NAME_INVALID: { - field: 'fullName', - fieldError: 'Incorrect name', - }, - REQ_INFO_PHONE_INVALID: { - field: 'phone', - fieldError: 'Incorrect phone', - }, - REQ_INFO_EMAIL_INVALID: { - field: 'email', - fieldError: 'Incorrect email', - }, -}; - - -export function getShippingErrors(dialogs: (ApiError | ApiInviteInfo)[]) { - return Object.values(dialogs).reduce((acc, cur) => { - if (!('hasErrorKey' in cur) || !cur.hasErrorKey) return acc; - const error = SHIPPING_ERRORS[cur.message]; - if (error) { - acc = { - ...acc, - [error.field]: error.fieldError, - }; - } - return acc; - }, {}); + const { message: description, code, param } = error; + const { field, message } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code]; + + return { field, message, description}; } diff --git a/src/modules/reducers/payments.ts b/src/modules/reducers/payments.ts index e5cc90502..846347c88 100644 --- a/src/modules/reducers/payments.ts +++ b/src/modules/reducers/payments.ts @@ -20,7 +20,7 @@ export function setRequestInfoId(global: GlobalState, id: string): GlobalState { ...global, payment: { ...global.payment, - formId: id, + requestId: id, }, }; } @@ -42,7 +42,9 @@ export function setInvoiceMessageInfo(global: GlobalState, message: ApiMessage): const { title, text, - description, + amount, + currency, + isTest, photoUrl, } = message.content.invoice; return { @@ -52,8 +54,10 @@ export function setInvoiceMessageInfo(global: GlobalState, message: ApiMessage): invoiceContent: { title, text, - description, photoUrl, + amount, + currency, + isTest, }, }, }; diff --git a/src/modules/selectors/payments.ts b/src/modules/selectors/payments.ts index 907a0a57a..98c1afbe9 100644 --- a/src/modules/selectors/payments.ts +++ b/src/modules/selectors/payments.ts @@ -1,14 +1,22 @@ import { GlobalState } from '../../global/types'; +export function selectPaymentChatId(global: GlobalState) { + return global.payment.chatId; +} + export function selectPaymentMessageId(global: GlobalState) { return global.payment.messageId; } -export function selectPaymentRequestId(global: GlobalState) { +export function selectPaymentFormId(global: GlobalState) { return global.payment.formId; } +export function selectPaymentRequestId(global: GlobalState) { + return global.payment.requestId; +} + export function selectProviderPublishableKey(global: GlobalState) { return global.payment.nativeParams ? global.payment.nativeParams.publishableKey : undefined; } diff --git a/src/util/formatCurrency.ts b/src/util/formatCurrency.ts new file mode 100644 index 000000000..8b3018a94 --- /dev/null +++ b/src/util/formatCurrency.ts @@ -0,0 +1,8 @@ +import { LangCode } from '../types'; + +export function formatCurrency(totalPrice: number, currency?: string, locale: LangCode = 'en') { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency, + }).format(currency === 'JPY' ? totalPrice : totalPrice / 100); +} diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index ed10b42b8..a430499f5 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -1,4 +1,4 @@ -import { ApiError } from '../api/types'; +import { ApiError, ApiFieldError } from '../api/types'; const READABLE_ERROR_MESSAGES: Record = { CHAT_RESTRICTED: 'You can\'t send messages in this chat, you were restricted', @@ -64,6 +64,45 @@ const READABLE_ERROR_MESSAGES: Record = { WALLPAPER_DIMENSIONS_INVALID: 'The wallpaper dimensions are invalid, please select another file', }; +export const SHIPPING_ERRORS: Record = { + ADDRESS_STREET_LINE1_INVALID: { + field: 'streetLine1', + message: 'Incorrect street address', + }, + ADDRESS_STREET_LINE2_INVALID: { + field: 'streetLine2', + message: 'Incorrect street address', + }, + ADDRESS_CITY_INVALID: { + field: 'city', + message: 'Incorrect city', + }, + ADDRESS_COUNTRY_INVALID: { + field: 'countryIso2', + message: 'Incorrect country', + }, + ADDRESS_POSTCODE_INVALID: { + field: 'postCode', + message: 'Incorrect post code', + }, + ADDRESS_STATE_INVALID: { + field: 'state', + message: 'Incorrect state', + }, + REQ_INFO_NAME_INVALID: { + field: 'fullName', + message: 'Incorrect name', + }, + REQ_INFO_PHONE_INVALID: { + field: 'phone', + message: 'Incorrect phone', + }, + REQ_INFO_EMAIL_INVALID: { + field: 'email', + message: 'Incorrect email', + }, +}; + export default function getReadableErrorText(error: ApiError) { const { message, isSlowMode, textParams } = error; // Currently, Telegram API doesn't return `SLOWMODE_WAIT_X` error as described in the docs @@ -79,3 +118,7 @@ export default function getReadableErrorText(error: ApiError) { } return errorMessage; } + +export function getShippingError(error: ApiError): ApiFieldError | undefined { + return SHIPPING_ERRORS[error.message]; +} diff --git a/src/util/langProvider.ts b/src/util/langProvider.ts index 88d9ddb9c..2b73c1e81 100644 --- a/src/util/langProvider.ts +++ b/src/util/langProvider.ts @@ -1,4 +1,7 @@ +import { getGlobal } from '../lib/teact/teactn'; + import { ApiLangPack, ApiLangString } from '../api/types'; +import { LangCode } from '../types'; import { DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS, @@ -7,13 +10,12 @@ import * as cacheApi from './cacheApi'; import { callApi } from '../api/gramjs'; import { createCallbackManager } from './callbacks'; import { formatInteger } from './textFormat'; -import { getGlobal } from '../lib/teact/teactn'; interface LangFn { (key: string, value?: any, format?: 'i'): any; isRtl?: boolean; - code?: string; + code?: LangCode; } const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g; @@ -95,7 +97,7 @@ export async function getTranslationForLangString(langCode: string, key: string) return processTranslation(translateString, key); } -export async function setLanguage(langCode: string, callback?: NoneToVoidFunction, withFallback = false) { +export async function setLanguage(langCode: LangCode, callback?: NoneToVoidFunction, withFallback = false) { if (langPack && langCode === currentLangCode) { if (callback) { callback();