Payment Modal: Support Payments 2.0 (#1375)

This commit is contained in:
Alexander Zinchuk 2021-08-16 14:21:22 +03:00
parent 94526b2362
commit af8248dcf2
32 changed files with 333 additions and 231 deletions

View File

@ -29,7 +29,6 @@ import { getApiChatIdFromMtpPeer } from './chats';
import { buildStickerFromDocument } from './symbols'; import { buildStickerFromDocument } from './symbols';
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
import { interpolateArray } from '../../../util/waveform'; import { interpolateArray } from '../../../util/waveform';
import { getCurrencySign } from '../../../components/middle/helpers/getCurrencySign';
import { buildPeer } from '../gramjsBuilders'; import { buildPeer } from '../gramjsBuilders';
import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers'; import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers';
@ -113,6 +112,7 @@ type UniversalMessage = (
export function buildApiMessageWithChatId(chatId: number, mtpMessage: UniversalMessage): ApiMessage { export function buildApiMessageWithChatId(chatId: number, mtpMessage: UniversalMessage): ApiMessage {
const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined; const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined;
const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined;
const isChatWithSelf = !fromId && chatId === currentUserId; const isChatWithSelf = !fromId && chatId === currentUserId;
const isOutgoing = (mtpMessage.out && !mtpMessage.post) || (isChatWithSelf && !mtpMessage.fwdFrom); 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) { if (action) {
content.action = action; content.action = action;
} }
@ -500,13 +501,15 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
const { const {
description: text, title, photo, test, totalAmount, currency, receiptMsgId, description: text, title, photo, test, totalAmount, currency, receiptMsgId,
} = media; } = media;
const currencySign = getCurrencySign(currency);
return { return {
text, text,
title, title,
photoUrl: photo && photo.url, photoUrl: photo && photo.url,
receiptMsgId, 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( function buildAction(
action: GramJs.TypeMessageAction, action: GramJs.TypeMessageAction,
senderId: number | undefined, senderId: number | undefined,
targetPeerId: number | undefined,
isChannelPost: boolean, isChannelPost: boolean,
isOutgoing: boolean, isOutgoing: boolean,
): ApiAction | undefined { ): ApiAction | undefined {
@ -574,7 +578,9 @@ function buildAction(
return undefined; return undefined;
} }
let text = ''; let amount: number | undefined;
let currency: string | undefined;
let text: string;
const translationValues = []; const translationValues = [];
let type: ApiAction['type'] = 'other'; let type: ApiAction['type'] = 'other';
let photo: ApiPhoto | undefined; let photo: ApiPhoto | undefined;
@ -661,10 +667,13 @@ function buildAction(
translationValues.push('%action_origin%'); translationValues.push('%action_origin%');
type = 'contactSignUp'; type = 'contactSignUp';
} else if (action instanceof GramJs.MessageActionPaymentSent) { } else if (action instanceof GramJs.MessageActionPaymentSent) {
const currencySign = getCurrencySign(action.currency); amount = Number(action.totalAmount);
const amount = (Number(action.totalAmount) / 100).toFixed(2); currency = action.currency;
text = 'Notification.PaymentSent'; text = 'PaymentSuccessfullyPaid';
translationValues.push(currencySign, amount, '%product%'); if (targetPeerId) {
targetUserIds.push(targetPeerId);
}
translationValues.push('%payment_amount%', '%target_user%', '%product%');
} else if (action instanceof GramJs.MessageActionGroupCall) { } else if (action instanceof GramJs.MessageActionGroupCall) {
if (action.duration) { if (action.duration) {
const mins = Math.max(Math.round(action.duration / 60), 1); const mins = Math.max(Math.round(action.duration / 60), 1);
@ -691,6 +700,8 @@ function buildAction(
targetUserIds, targetUserIds,
targetChatId, targetChatId,
photo, // TODO Only used internally now, will be used for the UI in future photo, // TODO Only used internally now, will be used for the UI in future
amount,
currency,
translationValues, translationValues,
}; };
} }
@ -739,7 +750,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
type = 'requestPoll'; type = 'requestPoll';
} else if (button instanceof GramJs.KeyboardButtonBuy) { } else if (button instanceof GramJs.KeyboardButtonBuy) {
if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) { if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) {
text = 'Receipt'; text = 'PaymentReceipt';
value = media.receiptMsgId; value = media.receiptMsgId;
} }
type = 'buy'; type = 'buy';

View File

@ -63,6 +63,7 @@ export function buildReceipt(receipt: GramJs.payments.PaymentReceipt) {
export function buildPaymentForm(form: GramJs.payments.PaymentForm) { export function buildPaymentForm(form: GramJs.payments.PaymentForm) {
const { const {
formId,
canSaveCredentials, canSaveCredentials,
passwordMissing, passwordMissing,
providerId, providerId,
@ -94,6 +95,7 @@ export function buildPaymentForm(form: GramJs.payments.PaymentForm) {
return { return {
canSaveCredentials, canSaveCredentials,
passwordMissing, passwordMissing,
formId: String(formId),
providerId, providerId,
nativeProvider, nativeProvider,
savedInfo, savedInfo,

View File

@ -1,13 +1,17 @@
import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
import { buildShippingInfo } from '../gramjsBuilders'; import { buildInputPeer, buildShippingInfo } from '../gramjsBuilders';
import { buildShippingOptions, buildPaymentForm, buildReceipt } from '../apiBuilders/payments'; import { buildShippingOptions, buildPaymentForm, buildReceipt } from '../apiBuilders/payments';
import { ApiChat } from '../../types';
export async function validateRequestedInfo({ export async function validateRequestedInfo({
chat,
messageId, messageId,
requestInfo, requestInfo,
shouldSave, shouldSave,
}: { }: {
chat: ApiChat;
messageId: number; messageId: number;
requestInfo: GramJs.TypePaymentRequestedInfo; requestInfo: GramJs.TypePaymentRequestedInfo;
shouldSave?: boolean; shouldSave?: boolean;
@ -16,6 +20,7 @@ export async function validateRequestedInfo({
shippingOptions: any; shippingOptions: any;
} | undefined> { } | undefined> {
const result = await invokeRequest(new GramJs.payments.ValidateRequestedInfo({ const result = await invokeRequest(new GramJs.payments.ValidateRequestedInfo({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: messageId, msgId: messageId,
save: shouldSave || undefined, save: shouldSave || undefined,
info: buildShippingInfo(requestInfo), info: buildShippingInfo(requestInfo),
@ -23,10 +28,12 @@ export async function validateRequestedInfo({
if (!result) { if (!result) {
return undefined; return undefined;
} }
const { id, shippingOptions } = result; const { id, shippingOptions } = result;
if (!id) { if (!id) {
return undefined; return undefined;
} }
return { return {
id, id,
shippingOptions: buildShippingOptions(shippingOptions), shippingOptions: buildShippingOptions(shippingOptions),
@ -34,17 +41,23 @@ export async function validateRequestedInfo({
} }
export function sendPaymentForm({ export function sendPaymentForm({
chat,
messageId, messageId,
formId,
requestedInfoId, requestedInfoId,
shippingOptionId, shippingOptionId,
credentials, credentials,
}: { }: {
chat: ApiChat;
messageId: number; messageId: number;
formId: string;
credentials: any; credentials: any;
requestedInfoId?: string; requestedInfoId?: string;
shippingOptionId?: string; shippingOptionId?: string;
}) { }) {
return invokeRequest(new GramJs.payments.SendPaymentForm({ return invokeRequest(new GramJs.payments.SendPaymentForm({
formId: BigInt(formId),
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: messageId, msgId: messageId,
requestedInfoId, requestedInfoId,
shippingOptionId, shippingOptionId,
@ -56,13 +69,16 @@ export function sendPaymentForm({
} }
export async function getPaymentForm({ export async function getPaymentForm({
messageId, chat, messageId,
}: { }: {
chat: ApiChat;
messageId: number; messageId: number;
}) { }) {
const result = await invokeRequest(new GramJs.payments.GetPaymentForm({ const result = await invokeRequest(new GramJs.payments.GetPaymentForm({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: messageId, msgId: messageId,
})); }));
if (!result) { if (!result) {
return undefined; return undefined;
} }
@ -70,10 +86,14 @@ export async function getPaymentForm({
return buildPaymentForm(result); return buildPaymentForm(result);
} }
export async function getReceipt(msgId: number) { export async function getReceipt(chat: ApiChat, msgId: number) {
const result = await invokeRequest(new GramJs.payments.GetPaymentReceipt({ msgId })); const result = await invokeRequest(new GramJs.payments.GetPaymentReceipt({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId,
}));
if (!result) { if (!result) {
return undefined; return undefined;
} }
return buildReceipt(result); return buildReceipt(result);
} }

View File

@ -131,8 +131,10 @@ export interface ApiInvoice {
text: string; text: string;
title: string; title: string;
photoUrl?: string; photoUrl?: string;
description?: string; amount: number;
currency: string;
receiptMsgId?: number; receiptMsgId?: number;
isTest?: boolean;
} }
export type ApiNewPoll = { export type ApiNewPoll = {
@ -150,6 +152,8 @@ export interface ApiAction {
targetChatId?: number; targetChatId?: number;
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'other'; type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'other';
photo?: ApiPhoto; photo?: ApiPhoto;
amount?: number;
currency?: string;
translationValues: string[]; translationValues: string[];
} }

View File

@ -318,6 +318,11 @@ export type ApiError = {
textParams?: Record<string, string>; textParams?: Record<string, string>;
}; };
export type ApiFieldError = {
field: string;
message: string;
};
export type ApiInviteInfo = { export type ApiInviteInfo = {
title: string; title: string;
hash: string; hash: string;

View File

@ -117,7 +117,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
const handleLangChange = useCallback(() => { const handleLangChange = useCallback(() => {
markIsLoading(); markIsLoading();
setLanguage(suggestedLanguage!, () => { void setLanguage(suggestedLanguage, () => {
unmarkIsLoading(); unmarkIsLoading();
setSettingOption({ language: suggestedLanguage }); setSettingOption({ language: suggestedLanguage });
@ -153,7 +153,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
if (!isPreloadInitiated) { if (!isPreloadInitiated) {
isPreloadInitiated = true; isPreloadInitiated = true;
preloadFonts(); preloadFonts();
preloadImage(monkeyPath); void preloadImage(monkeyPath);
} }
const { value, selectionStart, selectionEnd } = e.target; const { value, selectionStart, selectionEnd } = e.target;

View File

@ -64,7 +64,7 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
const handleLangChange = useCallback(() => { const handleLangChange = useCallback(() => {
markIsLoading(); markIsLoading();
setLanguage(suggestedLanguage!, () => { void setLanguage(suggestedLanguage, () => {
unmarkIsLoading(); unmarkIsLoading();
setSettingOption({ language: suggestedLanguage }); setSettingOption({ language: suggestedLanguage });

View File

@ -1,3 +1,5 @@
import { LangCode } from '../../../types';
export function getSuggestedLanguage() { export function getSuggestedLanguage() {
let suggestedLanguage = navigator.language; let suggestedLanguage = navigator.language;
@ -5,5 +7,5 @@ export function getSuggestedLanguage() {
suggestedLanguage = suggestedLanguage.substr(0, 2); suggestedLanguage = suggestedLanguage.substr(0, 2);
} }
return suggestedLanguage; return suggestedLanguage as LangCode;
} }

View File

@ -10,6 +10,7 @@ import {
isChat, isChat,
} from '../../../modules/helpers'; } from '../../../modules/helpers';
import trimText from '../../../util/trimText'; import trimText from '../../../util/trimText';
import { formatCurrency } from '../../../util/formatCurrency';
import { TextPart } from './renderMessageText'; import { TextPart } from './renderMessageText';
import renderText from './renderText'; import renderText from './renderText';
@ -37,16 +38,30 @@ export function renderActionMessageText(
if (!message.content.action) { if (!message.content.action) {
return []; return [];
} }
const { text, translationValues } = message.content.action; const {
text, translationValues, amount, currency,
} = message.content.action;
const content: TextPart[] = []; const content: TextPart[] = [];
const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 32 }; const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 32 };
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
? 'Message.PinnedGenericMessage' ? 'Message.PinnedGenericMessage'
: text; : text;
let unprocessed: string; let unprocessed = lang(translationKey, translationValues && translationValues.length ? translationValues : undefined);
let processed = processPlaceholder( let processed: TextPart[];
lang(translationKey, translationValues && translationValues.length ? translationValues : undefined),
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%', '%action_origin%',
actionOrigin actionOrigin
? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP ? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP

View File

@ -4,7 +4,7 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types'; import { GlobalActions } from '../../../global/types';
import { ISettings, SettingsScreens } from '../../../types'; import { ISettings, LangCode, SettingsScreens } from '../../../types';
import { ApiLanguage } from '../../../api/types'; import { ApiLanguage } from '../../../api/types';
import { setLanguage } from '../../../util/langProvider'; import { setLanguage } from '../../../util/langProvider';
@ -46,7 +46,7 @@ const SettingsLanguage: FC<OwnProps & StateProps & DispatchProps> = ({
setSelectedLanguage(langCode); setSelectedLanguage(langCode);
markIsLoading(); markIsLoading();
setLanguage(langCode, () => { void setLanguage(langCode as LangCode, () => {
unmarkIsLoading(); unmarkIsLoading();
setSettingOption({ language: langCode }); setSettingOption({ language: langCode });

View File

@ -1,17 +0,0 @@
const CURRENCIES: Record<string, string> = {
USD: '$',
EUR: '€',
GBP: '£',
JPY: '¥',
RUB: '₽',
UAH: '₴',
INR: '₹',
AED: 'د.إ',
};
export function getCurrencySign(currency: string | undefined): string {
if (!currency) {
return '';
}
return CURRENCIES[currency] || '';
}

View File

@ -48,12 +48,18 @@
} }
i { i {
font-size: 0.75rem; font-size: .875rem;
position: absolute; position: absolute;
right: 0.125rem; right: .1875rem;
top: 0.125rem; top: .1875rem;
display: block; display: block;
transform: rotate(-45deg);
&.icon-arrow-right {
font-size: .75rem;
top: .125rem;
right: .125rem;
transform: rotate(-45deg);
}
} }
} }

View File

@ -4,6 +4,7 @@ import { ApiKeyboardButton, ApiMessage } from '../../../api/types';
import { RE_TME_LINK } from '../../../config'; import { RE_TME_LINK } from '../../../config';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -15,6 +16,8 @@ type OwnProps = {
}; };
const InlineButtons: FC<OwnProps> = ({ message, onClick }) => { const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
const lang = useLang();
return ( return (
<div className="InlineButtons"> <div className="InlineButtons">
{message.inlineButtons!.map((row) => ( {message.inlineButtons!.map((row) => (
@ -26,7 +29,8 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
disabled={button.type === 'NOT_SUPPORTED'} disabled={button.type === 'NOT_SUPPORTED'}
onClick={() => onClick({ button })} onClick={() => onClick({ button })}
> >
{renderText(button.text)} {renderText(lang(button.text))}
{button.type === 'buy' && <i className="icon-card" />}
{button.type === 'url' && !button.value!.match(RE_TME_LINK) && <i className="icon-arrow-right" />} {button.type === 'url' && !button.value!.match(RE_TME_LINK) && <i className="icon-arrow-right" />}
</Button> </Button>
))} ))}

View File

@ -26,6 +26,10 @@
border-radius: var(--border-radius-messages-small); border-radius: var(--border-radius-messages-small);
color: var(--color-text); color: var(--color-text);
font-weight: 500; font-weight: 500;
span {
margin-left: .5rem;
}
} }
} }

View File

@ -3,7 +3,9 @@ import React, { FC, memo } from '../../../lib/teact/teact';
import { ApiMessage } from '../../../api/types'; import { ApiMessage } from '../../../api/types';
import { getMessageInvoice } from '../../../modules/helpers'; import { getMessageInvoice } from '../../../modules/helpers';
import { formatCurrency } from '../../../util/formatCurrency';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import './Invoice.scss'; import './Invoice.scss';
@ -14,12 +16,15 @@ type OwnProps = {
const Invoice: FC<OwnProps> = ({ const Invoice: FC<OwnProps> = ({
message, message,
}) => { }) => {
const lang = useLang();
const invoice = getMessageInvoice(message); const invoice = getMessageInvoice(message);
const { const {
title, title,
text, text,
description, amount,
currency,
isTest,
photoUrl, photoUrl,
} = invoice!; } = invoice!;
@ -41,9 +46,10 @@ const Invoice: FC<OwnProps> = ({
alt="" alt=""
/> />
)} )}
{description && ( <p className="description-text">
<p className="description-text">{renderText(description, ['emoji', 'br'])}</p> {formatCurrency(amount, currency, lang.code)}
)} {isTest && <span>{lang('PaymentTestInvoice')}</span>}
</p>
</div> </div>
</div> </div>
); );

View File

@ -716,11 +716,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
onCancelMediaTransfer={handleCancelUpload} onCancelMediaTransfer={handleCancelUpload}
/> />
)} )}
{invoice && ( {invoice && <Invoice message={message} />}
<Invoice
message={message}
/>
)}
</div> </div>
); );
} }

View File

@ -2,14 +2,16 @@ import React, {
FC, memo, FC, memo,
} from '../../lib/teact/teact'; } 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'; import './Checkout.scss';
export type OwnProps = { export type OwnProps = {
invoiceContent?: { invoiceContent?: {
title?: string; title?: string;
description?: string;
text?: string; text?: string;
photoUrl?: string; photoUrl?: string;
}; };
@ -35,8 +37,9 @@ const Checkout: FC<OwnProps> = ({
currency, currency,
totalPrice, totalPrice,
}) => { }) => {
// eslint-disable-next-line no-null/no-null const lang = useLang();
const { photoUrl, title, text } = (invoiceContent || {});
const { photoUrl, title, text } = invoiceContent || {};
const { const {
paymentMethod, paymentMethod,
paymentProvider, paymentProvider,
@ -45,26 +48,25 @@ const Checkout: FC<OwnProps> = ({
phone, phone,
shippingMethod, shippingMethod,
} = (checkoutInfo || {}); } = (checkoutInfo || {});
return ( return (
<div className="Checkout"> <div className="Checkout">
<div className="description has-image"> <div className="description has-image">
{ photoUrl && ( {photoUrl && <img src={photoUrl} alt="" />}
<img src={photoUrl} alt="" />
)}
<div className="text"> <div className="text">
<h5>{ title }</h5> <h5>{title}</h5>
<p>{ text }</p> <p>{text}</p>
</div> </div>
</div> </div>
<div className="price-info"> <div className="price-info">
{ prices && prices.map((item) => ( { prices && prices.map((item) => (
renderPaymentItem(item.label, item.amount, currency, false) renderPaymentItem(lang.code, item.label, item.amount, currency)
)) } )) }
{ shippingPrices && shippingPrices.map((item) => ( { shippingPrices && shippingPrices.map((item) => (
renderPaymentItem(item.label, item.amount, currency, false) renderPaymentItem(lang.code, item.label, item.amount, currency)
)) } )) }
{ totalPrice !== undefined && ( { totalPrice !== undefined && (
renderPaymentItem('Total', totalPrice, currency, true) renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, currency, true)
) } ) }
</div> </div>
<div className="invoice-info"> <div className="invoice-info">
@ -79,14 +81,16 @@ const Checkout: FC<OwnProps> = ({
); );
}; };
function renderPaymentItem(title: string, value: number, currency?: string, main = false) { function renderPaymentItem(
langCode: LangCode | undefined, title: string, value: number, currency?: string, main = false,
) {
return ( return (
<div className={`price-info-item ${main ? 'price-info-item-main' : ''}`}> <div className={`price-info-item ${main ? 'price-info-item-main' : ''}`}>
<div className="title"> <div className="title">
{ title } { title }
</div> </div>
<div className="value"> <div className="value">
{ `${currency || ''} ${(value / 100).toFixed(2)}` } {formatCurrency(value, currency, langCode)}
</div> </div>
</div> </div>
); );

View File

@ -5,12 +5,10 @@ import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions, GlobalState } from '../../global/types'; import { GlobalActions, GlobalState } from '../../global/types';
import { PaymentStep, ShippingOption, Price } from '../../types'; import { PaymentStep, ShippingOption, Price } from '../../types';
import { ApiError, ApiInviteInfo } from '../../api/types';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { getCurrencySign } from '../middle/helpers/getCurrencySign'; import { formatCurrency } from '../../util/formatCurrency';
import { detectCardTypeText } from '../common/helpers/detectCardType'; import { detectCardTypeText } from '../common/helpers/detectCardType';
import { getShippingErrors } from '../../modules/helpers/payments';
import usePaymentReducer, { FormState } from '../../hooks/reducers/usePaymentReducer'; import usePaymentReducer, { FormState } from '../../hooks/reducers/usePaymentReducer';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -46,7 +44,6 @@ type StateProps = {
needCardholderName?: boolean; needCardholderName?: boolean;
needCountry?: boolean; needCountry?: boolean;
needZip?: boolean; needZip?: boolean;
globalDialogs?: (ApiError | ApiInviteInfo)[];
}; };
type GlobalStateProps = Pick<GlobalState['payment'], 'step' | 'shippingOptions' | type GlobalStateProps = Pick<GlobalState['payment'], 'step' | 'shippingOptions' |
@ -79,7 +76,6 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
needCountry, needCountry,
needZip, needZip,
error, error,
globalDialogs,
validateRequestedInfo, validateRequestedInfo,
sendPaymentForm, sendPaymentForm,
setPaymentStep, setPaymentStep,
@ -87,36 +83,26 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
clearPaymentError, clearPaymentError,
}) => { }) => {
const [paymentState, paymentDispatch] = usePaymentReducer(); const [paymentState, paymentDispatch] = usePaymentReducer();
const currencySign = getCurrencySign(currency);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const lang = useLang(); const lang = useLang();
useEffect(() => { useEffect(() => {
if (step || error || globalDialogs) { if (step || error) {
setIsLoading(false); setIsLoading(false);
} }
}, [step, error, globalDialogs]); }, [step, error]);
useEffect(() => { useEffect(() => {
if (error && error.field) { if (error && error.field) {
paymentDispatch({ paymentDispatch({
type: 'setFormErrors', type: 'setFormErrors',
payload: { payload: {
[error.field]: error.fieldError, [error.field]: error.message,
}, },
}); });
return; return;
} }
if (globalDialogs && globalDialogs.length) { }, [error, paymentDispatch]);
const errors = getShippingErrors(globalDialogs);
paymentDispatch({
type: 'setFormErrors',
payload: {
...errors,
},
});
}
}, [error, globalDialogs, paymentDispatch]);
useEffect(() => { useEffect(() => {
if (savedInfo) { if (savedInfo) {
@ -178,8 +164,8 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
); );
} }
function renderModalContent(cuurentStep: PaymentStep) { function renderModalContent(currentStep: PaymentStep) {
switch (cuurentStep) { switch (currentStep) {
case PaymentStep.ShippingInfo: case PaymentStep.ShippingInfo:
return ( return (
<ShippingInfo <ShippingInfo
@ -197,7 +183,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
state={paymentState} state={paymentState}
dispatch={paymentDispatch} dispatch={paymentDispatch}
shippingOptions={shippingOptions || []} shippingOptions={shippingOptions || []}
currency={currencySign} currency={currency}
/> />
); );
case PaymentStep.PaymentInfo: case PaymentStep.PaymentInfo:
@ -221,7 +207,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
totalPrice={totalPrice} totalPrice={totalPrice}
invoiceContent={invoiceContent} invoiceContent={invoiceContent}
checkoutInfo={checkoutInfo} checkoutInfo={checkoutInfo}
currency={currencySign} currency={currency}
/> />
); );
default: default:
@ -287,11 +273,11 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
switch (step) { switch (step) {
case PaymentStep.Checkout: case PaymentStep.Checkout:
return lang('Checkout.PayPrice', `${currencySign}${(totalPrice / 100).toFixed(2)}`); return lang('Checkout.PayPrice', formatCurrency(totalPrice, currency, lang.code));
default: default:
return lang('Next'); return lang('Next');
} }
}, [step, lang, currencySign, totalPrice]); }, [step, lang, currency, totalPrice]);
if (isProviderError) { if (isProviderError) {
return ( return (
@ -412,7 +398,6 @@ export default memo(withGlobal<OwnProps>(
needCountry, needCountry,
needZip, needZip,
error, error,
globalDialogs: global.dialogs,
}; };
}, },
(setGlobal, actions): DispatchProps => { (setGlobal, actions): DispatchProps => {

View File

@ -4,10 +4,9 @@ import React, {
import { withGlobal } from '../../lib/teact/teactn'; import { withGlobal } from '../../lib/teact/teactn';
import { Price } from '../../types'; import { Price } from '../../types';
import { ApiShippingAddress } from '../../api/types/payments'; import { ApiShippingAddress } from '../../api/types';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { getCurrencySign } from '../middle/helpers/getCurrencySign';
import Checkout from './Checkout'; import Checkout from './Checkout';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
@ -52,10 +51,10 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
shippingMethod, shippingMethod,
}) => { }) => {
const lang = useLang(); const lang = useLang();
const currencySign = getCurrencySign(currency);
const checkoutInfo = useMemo(() => { const checkoutInfo = useMemo(() => {
return getCheckoutInfo(credentialsTitle, info, shippingMethod); return getCheckoutInfo(credentialsTitle, info, shippingMethod);
}, [info, shippingMethod, credentialsTitle]); }, [info, shippingMethod, credentialsTitle]);
return ( return (
<Modal <Modal
className="PaymentModal PaymentModal-receipt" className="PaymentModal PaymentModal-receipt"
@ -87,7 +86,7 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
title, title,
}} }}
checkoutInfo={checkoutInfo} checkoutInfo={checkoutInfo}
currency={currencySign} currency={currency}
/> />
</div> </div>
</div> </div>
@ -100,7 +99,7 @@ export default memo(withGlobal<OwnProps>(
const { receipt } = global.payment; const { receipt } = global.payment;
const { const {
currency, currency,
prices: mapedPrices, prices,
info, info,
totalAmount, totalAmount,
credentialsTitle, credentialsTitle,
@ -113,7 +112,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
currency, currency,
prices: mapedPrices, prices,
info, info,
totalAmount, totalAmount,
credentialsTitle, credentialsTitle,

View File

@ -2,9 +2,11 @@ import React, {
FC, useCallback, memo, useMemo, useEffect, FC, useCallback, memo, useMemo, useEffect,
} from '../../lib/teact/teact'; } 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 { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
import useLang from '../../hooks/useLang';
import RadioGroup from '../ui/RadioGroup'; import RadioGroup from '../ui/RadioGroup';
@ -13,7 +15,7 @@ import './Shipping.scss';
export type OwnProps = { export type OwnProps = {
state: FormState; state: FormState;
shippingOptions: ShippingOption[]; shippingOptions: ShippingOption[];
currency: string; currency?: string;
dispatch: FormEditDispatch; dispatch: FormEditDispatch;
}; };
@ -23,6 +25,8 @@ const Shipping: FC<OwnProps> = ({
currency, currency,
dispatch, dispatch,
}) => { }) => {
const lang = useLang();
useEffect(() => { useEffect(() => {
if (!shippingOptions || state.shipping) { if (!shippingOptions || state.shipping) {
return; return;
@ -36,9 +40,9 @@ const Shipping: FC<OwnProps> = ({
const options = useMemo(() => (shippingOptions.map(({ id: value, title: label, amount }) => ({ const options = useMemo(() => (shippingOptions.map(({ id: value, title: label, amount }) => ({
label, label,
subLabel: `${currency} ${String(amount / 100)}`, subLabel: formatCurrency(amount, currency, lang.code),
value, value,
}))), [shippingOptions, currency]); }))), [shippingOptions, currency, lang.code]);
return ( return (
<div className="Shipping"> <div className="Shipping">

View File

@ -19,6 +19,7 @@ import {
ApiSession, ApiSession,
ApiNewPoll, ApiNewPoll,
ApiInviteInfo, ApiInviteInfo,
ApiFieldError,
} from '../api/types'; } from '../api/types';
import { import {
FocusDirection, FocusDirection,
@ -344,18 +345,22 @@ export type GlobalState = {
}; };
payment: { payment: {
chatId?: number;
messageId?: number; messageId?: number;
step?: PaymentStep; step?: PaymentStep;
shippingOptions?: ShippingOption[]; shippingOptions?: ShippingOption[];
formId?: string; formId?: string;
requestId?: string;
savedInfo?: ApiPaymentSavedInfo; savedInfo?: ApiPaymentSavedInfo;
canSaveCredentials?: boolean; canSaveCredentials?: boolean;
invoice?: Invoice; invoice?: Invoice;
invoiceContent?: { invoiceContent?: {
title?: string; title?: string;
text?: string; text?: string;
description?: string;
photoUrl?: string; photoUrl?: string;
amount?: number;
currency?: string;
isTest?: boolean;
}; };
nativeProvider?: string; nativeProvider?: string;
providerId?: number; providerId?: number;
@ -377,7 +382,7 @@ export type GlobalState = {
receipt?: Receipt; receipt?: Receipt;
error?: { error?: {
field?: string; field?: string;
fieldError?: string; message?: string;
description: string; description: string;
}; };
isPaymentModalOpen?: boolean; isPaymentModalOpen?: boolean;
@ -501,7 +506,7 @@ export type ActionTypes = (
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
'deleteDeviceToken' | 'deleteDeviceToken' |
// payment // payment
'openPaymentModal' | 'closePaymentModal' | 'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' | 'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |
'sendCredentialsInfo' | 'setInvoiceMessageInfo' | 'clearPaymentError' | 'clearReceipt' 'sendCredentialsInfo' | 'setInvoiceMessageInfo' | 'clearPaymentError' | 'clearReceipt'
); );

View File

@ -55,9 +55,9 @@ addReducer('clickInlineButton', (global, actions, payload) => {
if (value) { if (value) {
actions.getReceipt({ receiptMessageId: value, chatId: chat.id, messageId }); actions.getReceipt({ receiptMessageId: value, chatId: chat.id, messageId });
} else { } else {
actions.getPaymentForm({ messageId }); actions.getPaymentForm({ chat, messageId });
actions.setInvoiceMessageInfo(selectChatMessage(global, chat.id, messageId)); actions.setInvoiceMessageInfo(selectChatMessage(global, chat.id, messageId));
actions.openPaymentModal({ messageId }); actions.openPaymentModal({ chatId: chat.id, messageId });
} }
break; break;
} }

View File

@ -1,16 +1,20 @@
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn'; import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import { PaymentStep } from '../../../types/index'; import { PaymentStep } from '../../../types';
import { callApi } from '../../../api/gramjs'; import { ApiChat } from '../../../api/types';
import { import {
selectPaymentMessageId, selectPaymentMessageId,
selectPaymentRequestId, selectPaymentRequestId,
selectProviderPublishableKey, selectProviderPublishableKey,
selectStripeCredentials, selectStripeCredentials,
selectChatMessage, selectChatMessage,
selectPaymentChatId,
selectChat,
selectPaymentFormId,
} from '../../selectors'; } from '../../selectors';
import { callApi } from '../../../api/gramjs';
import { getStripeError } from '../../helpers/payments'; import { getStripeError } from '../../helpers';
import { buildQueryString } from '../../../util/requestQuery'; import { buildQueryString } from '../../../util/requestQuery';
import { import {
@ -27,22 +31,28 @@ import {
addReducer('validateRequestedInfo', (global, actions, payload) => { addReducer('validateRequestedInfo', (global, actions, payload) => {
const { requestInfo, saveInfo } = payload; const { requestInfo, saveInfo } = payload;
const chatId = selectPaymentChatId(global);
const chat = chatId && selectChat(global, chatId);
const messageId = selectPaymentMessageId(global); const messageId = selectPaymentMessageId(global);
if (!messageId) { if (!chat || !messageId) {
return; return;
} }
validateRequestedInfo(messageId, requestInfo, saveInfo); void validateRequestedInfo(chat, messageId, requestInfo, saveInfo);
}); });
async function validateRequestedInfo(messageId: number, requestInfo: any, shouldSave?: true) { async function validateRequestedInfo(chat: ApiChat, messageId: number, requestInfo: any, shouldSave?: true) {
const result = await callApi('validateRequestedInfo', { messageId, requestInfo, shouldSave }); const result = await callApi('validateRequestedInfo', {
chat, messageId, requestInfo, shouldSave,
});
if (!result) { if (!result) {
return; return;
} }
const { id, shippingOptions } = result; const { id, shippingOptions } = result;
if (!id) { if (!id) {
return; return;
} }
let global = setRequestInfoId(getGlobal(), id); let global = setRequestInfoId(getGlobal(), id);
if (shippingOptions) { if (shippingOptions) {
global = updateShippingOptions(global, shippingOptions); global = updateShippingOptions(global, shippingOptions);
@ -54,16 +64,16 @@ async function validateRequestedInfo(messageId: number, requestInfo: any, should
} }
addReducer('getPaymentForm', (global, actions, payload) => { addReducer('getPaymentForm', (global, actions, payload) => {
const { messageId } = payload; const { chat, messageId } = payload;
if (!messageId) { if (!chat || !messageId) {
return; return;
} }
getPaymentForm(messageId); void getPaymentForm(chat, messageId);
}); });
async function getPaymentForm(messageId: number) { async function getPaymentForm(chat: ApiChat, messageId: number) {
const result = await callApi('getPaymentForm', { messageId }); const result = await callApi('getPaymentForm', { chat, messageId });
if (!result) { if (!result) {
return; return;
} }
@ -82,19 +92,22 @@ async function getPaymentForm(messageId: number) {
addReducer('getReceipt', (global, actions, payload) => { addReducer('getReceipt', (global, actions, payload) => {
const { receiptMessageId, chatId, messageId } = payload; const { receiptMessageId, chatId, messageId } = payload;
if (!messageId || !receiptMessageId || !chatId) { const chat = chatId && selectChat(global, chatId);
if (!messageId || !receiptMessageId || !chat) {
return; return;
} }
getReceipt(messageId, receiptMessageId, chatId);
void getReceipt(chat, messageId, receiptMessageId);
}); });
async function getReceipt(messageId: number, receiptMessageId: number, chatId: number) { async function getReceipt(chat: ApiChat, messageId: number, receiptMessageId: number) {
const result = await callApi('getReceipt', receiptMessageId); const result = await callApi('getReceipt', chat, receiptMessageId);
if (!result) { if (!result) {
return; return;
} }
let global = getGlobal(); let global = getGlobal();
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chat.id, messageId);
global = setReceipt(global, result, message); global = setReceipt(global, result, message);
setGlobal(global); setGlobal(global);
} }
@ -126,34 +139,40 @@ addReducer('sendCredentialsInfo', (global, actions, payload) => {
} }
const { credentials } = payload; const { credentials } = payload;
const { data } = credentials; const { data } = credentials;
sendStipeCredentials(data, publishableKey); void sendStripeCredentials(data, publishableKey);
}); });
addReducer('sendPaymentForm', (global, actions, payload) => { addReducer('sendPaymentForm', (global, actions, payload) => {
const { shippingOptionId, saveCredentials } = payload; const { shippingOptionId, saveCredentials } = payload;
const chatId = selectPaymentChatId(global);
const chat = chatId && selectChat(global, chatId);
const messageId = selectPaymentMessageId(global); const messageId = selectPaymentMessageId(global);
const formId = selectPaymentFormId(global);
const requestInfoId = selectPaymentRequestId(global); const requestInfoId = selectPaymentRequestId(global);
const publishableKey = selectProviderPublishableKey(global); const publishableKey = selectProviderPublishableKey(global);
const stripeCredentials = selectStripeCredentials(global); const stripeCredentials = selectStripeCredentials(global);
if (!messageId || !publishableKey) { if (!chat || !messageId || !publishableKey || !formId) {
return; return;
} }
sendPaymentForm(messageId, {
void sendPaymentForm(chat, messageId, formId, {
save: saveCredentials, save: saveCredentials,
data: stripeCredentials, data: stripeCredentials,
}, requestInfoId, shippingOptionId); }, requestInfoId, shippingOptionId);
}); });
async function sendStipeCredentials(data: { async function sendStripeCredentials(
cardNumber: string; data: {
cardholder?: string; cardNumber: string;
expiryMonth: string; cardholder?: string;
expiryYear: string; expiryMonth: string;
cvv: string; expiryYear: string;
country: string; cvv: string;
zip: string; country: string;
}, zip: string;
publishableKey: string) { },
publishableKey: string,
) {
const query = buildQueryString({ const query = buildQueryString({
'card[number]': data.cardNumber, 'card[number]': data.cardNumber,
'card[exp_month]': data.expiryMonth, 'card[exp_month]': data.expiryMonth,
@ -195,13 +214,15 @@ publishableKey: string) {
} }
async function sendPaymentForm( async function sendPaymentForm(
chat: ApiChat,
messageId: number, messageId: number,
formId: string,
credentials: any, credentials: any,
requestedInfoId?: string, requestedInfoId?: string,
shippingOptionId?: string, shippingOptionId?: string,
) { ) {
const result = await callApi('sendPaymentForm', { const result = await callApi('sendPaymentForm', {
messageId, credentials, requestedInfoId, shippingOptionId, chat, messageId, formId, credentials, requestedInfoId, shippingOptionId,
}); });
if (result) { if (result) {
const global = clearPayment(getGlobal()); const global = clearPayment(getGlobal());

View File

@ -18,6 +18,7 @@ import { updateUser } from '../../reducers';
import { setLanguage } from '../../../util/langProvider'; import { setLanguage } from '../../../util/langProvider';
import { selectNotifySettings } from '../../selectors'; import { selectNotifySettings } from '../../selectors';
import { forceWebsync } from '../../../util/websync'; import { forceWebsync } from '../../../util/websync';
import { getShippingError } from '../../../util/getReadableErrorText';
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
if (DEBUG) { if (DEBUG) {
@ -61,7 +62,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
actions.signOut(); 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 } }); actions.showDialog({ data: { ...update.error, hasErrorKey: true } });
} }
@ -71,8 +75,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
function onUpdateApiReady(global: GlobalState) { function onUpdateApiReady(global: GlobalState) {
const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global); const { hasWebNotifications, hasPushNotifications } = selectNotifySettings(global);
if (hasWebNotifications && hasPushNotifications) subscribe(); if (hasWebNotifications && hasPushNotifications) {
setLanguage(global.settings.byKey.language); void subscribe();
}
void setLanguage(global.settings.byKey.language);
} }
function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) { function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) {

View File

@ -17,7 +17,7 @@ addReducer('init', (global) => {
const { animationLevel, messageTextSize, language } = global.settings.byKey; const { animationLevel, messageTextSize, language } = global.settings.byKey;
const theme = selectTheme(global); const theme = selectTheme(global);
setLanguage(language, undefined, true); void setLanguage(language, undefined, true);
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`, '--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,

View File

@ -1,14 +1,14 @@
import { addReducer } from '../../../lib/teact/teactn'; import { addReducer } from '../../../lib/teact/teactn';
import {
clearPayment, closeInvoice, import { clearPayment, closeInvoice } from '../../reducers';
} from '../../reducers';
addReducer('openPaymentModal', (global, actions, payload) => { addReducer('openPaymentModal', (global, actions, payload) => {
const { messageId } = payload; const { chatId, messageId } = payload;
return { return {
...global, ...global,
payment: { payment: {
...global.payment, ...global.payment,
chatId,
messageId, messageId,
isPaymentModalOpen: true, isPaymentModalOpen: true,
}, },
@ -19,3 +19,15 @@ addReducer('closePaymentModal', (global) => {
const newGlobal = clearPayment(global); const newGlobal = clearPayment(global);
return closeInvoice(newGlobal); return closeInvoice(newGlobal);
}); });
addReducer('addPaymentError', (global, actions, payload) => {
const { error } = payload!;
return {
...global,
payment: {
...global.payment,
error,
},
};
});

View File

@ -1,41 +1,41 @@
import { ApiError, ApiInviteInfo } from '../../api/types'; import { ApiFieldError } from '../../api/types';
const STRIPE_ERRORS: Record<string, Record<string, string>> = { const STRIPE_ERRORS: Record<string, ApiFieldError> = {
missing_payment_information: { missing_payment_information: {
field: 'cardNumber', field: 'cardNumber',
fieldError: 'Incorrect card number', message: 'Incorrect card number',
}, },
invalid_number: { invalid_number: {
field: 'cardNumber', field: 'cardNumber',
fieldError: 'Incorrect card number', message: 'Incorrect card number',
}, },
number: { number: {
field: 'cardNumber', field: 'cardNumber',
fieldError: 'Incorrect card number', message: 'Incorrect card number',
}, },
exp_year: { exp_year: {
field: 'expiry', field: 'expiry',
fieldError: 'Incorrect year', message: 'Incorrect year',
}, },
exp_month: { exp_month: {
field: 'expiry', field: 'expiry',
fieldError: 'Incorrect month', message: 'Incorrect month',
}, },
invalid_expiry_year: { invalid_expiry_year: {
field: 'expiry', field: 'expiry',
fieldError: 'Incorrect year', message: 'Incorrect year',
}, },
invalid_expiry_month: { invalid_expiry_month: {
field: 'expiry', field: 'expiry',
fieldError: 'Incorrect month', message: 'Incorrect month',
}, },
cvc: { cvc: {
field: 'cvv', field: 'cvv',
fieldError: 'Incorrect CVV', message: 'Incorrect CVV',
}, },
invalid_cvc: { invalid_cvc: {
field: 'cvv', field: 'cvv',
fieldError: 'Incorrect CVV', message: 'Incorrect CVV',
}, },
}; };
@ -44,65 +44,8 @@ export function getStripeError(error: {
message: string; message: string;
param?: string; param?: string;
}) { }) {
const { message, code, param } = error; const { message: description, code, param } = error;
const { field, fieldError, description } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code]; const { field, message } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code];
return {
field, return { field, message, description};
fieldError,
description: description || message,
};
}
const SHIPPING_ERRORS: Record<string, Record<string, string>> = {
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;
}, {});
} }

View File

@ -20,7 +20,7 @@ export function setRequestInfoId(global: GlobalState, id: string): GlobalState {
...global, ...global,
payment: { payment: {
...global.payment, ...global.payment,
formId: id, requestId: id,
}, },
}; };
} }
@ -42,7 +42,9 @@ export function setInvoiceMessageInfo(global: GlobalState, message: ApiMessage):
const { const {
title, title,
text, text,
description, amount,
currency,
isTest,
photoUrl, photoUrl,
} = message.content.invoice; } = message.content.invoice;
return { return {
@ -52,8 +54,10 @@ export function setInvoiceMessageInfo(global: GlobalState, message: ApiMessage):
invoiceContent: { invoiceContent: {
title, title,
text, text,
description,
photoUrl, photoUrl,
amount,
currency,
isTest,
}, },
}, },
}; };

View File

@ -1,14 +1,22 @@
import { GlobalState } from '../../global/types'; import { GlobalState } from '../../global/types';
export function selectPaymentChatId(global: GlobalState) {
return global.payment.chatId;
}
export function selectPaymentMessageId(global: GlobalState) { export function selectPaymentMessageId(global: GlobalState) {
return global.payment.messageId; return global.payment.messageId;
} }
export function selectPaymentRequestId(global: GlobalState) { export function selectPaymentFormId(global: GlobalState) {
return global.payment.formId; return global.payment.formId;
} }
export function selectPaymentRequestId(global: GlobalState) {
return global.payment.requestId;
}
export function selectProviderPublishableKey(global: GlobalState) { export function selectProviderPublishableKey(global: GlobalState) {
return global.payment.nativeParams ? global.payment.nativeParams.publishableKey : undefined; return global.payment.nativeParams ? global.payment.nativeParams.publishableKey : undefined;
} }

View File

@ -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);
}

View File

@ -1,4 +1,4 @@
import { ApiError } from '../api/types'; import { ApiError, ApiFieldError } from '../api/types';
const READABLE_ERROR_MESSAGES: Record<string, string> = { const READABLE_ERROR_MESSAGES: Record<string, string> = {
CHAT_RESTRICTED: 'You can\'t send messages in this chat, you were restricted', CHAT_RESTRICTED: 'You can\'t send messages in this chat, you were restricted',
@ -64,6 +64,45 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
WALLPAPER_DIMENSIONS_INVALID: 'The wallpaper dimensions are invalid, please select another file', WALLPAPER_DIMENSIONS_INVALID: 'The wallpaper dimensions are invalid, please select another file',
}; };
export const SHIPPING_ERRORS: Record<string, ApiFieldError> = {
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) { export default function getReadableErrorText(error: ApiError) {
const { message, isSlowMode, textParams } = error; const { message, isSlowMode, textParams } = error;
// Currently, Telegram API doesn't return `SLOWMODE_WAIT_X` error as described in the docs // 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; return errorMessage;
} }
export function getShippingError(error: ApiError): ApiFieldError | undefined {
return SHIPPING_ERRORS[error.message];
}

View File

@ -1,4 +1,7 @@
import { getGlobal } from '../lib/teact/teactn';
import { ApiLangPack, ApiLangString } from '../api/types'; import { ApiLangPack, ApiLangString } from '../api/types';
import { LangCode } from '../types';
import { import {
DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS, 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 { callApi } from '../api/gramjs';
import { createCallbackManager } from './callbacks'; import { createCallbackManager } from './callbacks';
import { formatInteger } from './textFormat'; import { formatInteger } from './textFormat';
import { getGlobal } from '../lib/teact/teactn';
interface LangFn { interface LangFn {
(key: string, value?: any, format?: 'i'): any; (key: string, value?: any, format?: 'i'): any;
isRtl?: boolean; isRtl?: boolean;
code?: string; code?: LangCode;
} }
const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g; const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g;
@ -95,7 +97,7 @@ export async function getTranslationForLangString(langCode: string, key: string)
return processTranslation(translateString, key); 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 (langPack && langCode === currentLangCode) {
if (callback) { if (callback) {
callback(); callback();