Payment Modal: Support Payments 2.0 (#1375)
This commit is contained in:
parent
94526b2362
commit
af8248dcf2
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@ -318,6 +318,11 @@ export type ApiError = {
|
||||
textParams?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type ApiFieldError = {
|
||||
field: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ApiInviteInfo = {
|
||||
title: string;
|
||||
hash: string;
|
||||
|
||||
@ -117,7 +117,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
const handleLangChange = useCallback(() => {
|
||||
markIsLoading();
|
||||
|
||||
setLanguage(suggestedLanguage!, () => {
|
||||
void setLanguage(suggestedLanguage, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: suggestedLanguage });
|
||||
@ -153,7 +153,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
if (!isPreloadInitiated) {
|
||||
isPreloadInitiated = true;
|
||||
preloadFonts();
|
||||
preloadImage(monkeyPath);
|
||||
void preloadImage(monkeyPath);
|
||||
}
|
||||
|
||||
const { value, selectionStart, selectionEnd } = e.target;
|
||||
|
||||
@ -64,7 +64,7 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
const handleLangChange = useCallback(() => {
|
||||
markIsLoading();
|
||||
|
||||
setLanguage(suggestedLanguage!, () => {
|
||||
void setLanguage(suggestedLanguage, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: suggestedLanguage });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<OwnProps & StateProps & DispatchProps> = ({
|
||||
setSelectedLanguage(langCode);
|
||||
markIsLoading();
|
||||
|
||||
setLanguage(langCode, () => {
|
||||
void setLanguage(langCode as LangCode, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: langCode });
|
||||
|
||||
@ -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] || '';
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<OwnProps> = ({ message, onClick }) => {
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<div className="InlineButtons">
|
||||
{message.inlineButtons!.map((row) => (
|
||||
@ -26,7 +29,8 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
disabled={button.type === 'NOT_SUPPORTED'}
|
||||
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>
|
||||
))}
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
border-radius: var(--border-radius-messages-small);
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
|
||||
span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
message,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const invoice = getMessageInvoice(message);
|
||||
|
||||
const {
|
||||
title,
|
||||
text,
|
||||
description,
|
||||
amount,
|
||||
currency,
|
||||
isTest,
|
||||
photoUrl,
|
||||
} = invoice!;
|
||||
|
||||
@ -41,9 +46,10 @@ const Invoice: FC<OwnProps> = ({
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
{description && (
|
||||
<p className="description-text">{renderText(description, ['emoji', 'br'])}</p>
|
||||
)}
|
||||
<p className="description-text">
|
||||
{formatCurrency(amount, currency, lang.code)}
|
||||
{isTest && <span>{lang('PaymentTestInvoice')}</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -716,11 +716,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
onCancelMediaTransfer={handleCancelUpload}
|
||||
/>
|
||||
)}
|
||||
{invoice && (
|
||||
<Invoice
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
{invoice && <Invoice message={message} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
phone,
|
||||
shippingMethod,
|
||||
} = (checkoutInfo || {});
|
||||
|
||||
return (
|
||||
<div className="Checkout">
|
||||
<div className="description has-image">
|
||||
{ photoUrl && (
|
||||
<img src={photoUrl} alt="" />
|
||||
)}
|
||||
{photoUrl && <img src={photoUrl} alt="" />}
|
||||
<div className="text">
|
||||
<h5>{ title }</h5>
|
||||
<p>{ text }</p>
|
||||
<h5>{title}</h5>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="price-info">
|
||||
{ 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)
|
||||
) }
|
||||
</div>
|
||||
<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 (
|
||||
<div className={`price-info-item ${main ? 'price-info-item-main' : ''}`}>
|
||||
<div className="title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="value">
|
||||
{ `${currency || ''} ${(value / 100).toFixed(2)}` }
|
||||
{formatCurrency(value, currency, langCode)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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<GlobalState['payment'], 'step' | 'shippingOptions' |
|
||||
@ -79,7 +76,6 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
needCountry,
|
||||
needZip,
|
||||
error,
|
||||
globalDialogs,
|
||||
validateRequestedInfo,
|
||||
sendPaymentForm,
|
||||
setPaymentStep,
|
||||
@ -87,36 +83,26 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
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<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderModalContent(cuurentStep: PaymentStep) {
|
||||
switch (cuurentStep) {
|
||||
function renderModalContent(currentStep: PaymentStep) {
|
||||
switch (currentStep) {
|
||||
case PaymentStep.ShippingInfo:
|
||||
return (
|
||||
<ShippingInfo
|
||||
@ -197,7 +183,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
state={paymentState}
|
||||
dispatch={paymentDispatch}
|
||||
shippingOptions={shippingOptions || []}
|
||||
currency={currencySign}
|
||||
currency={currency}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.PaymentInfo:
|
||||
@ -221,7 +207,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
totalPrice={totalPrice}
|
||||
invoiceContent={invoiceContent}
|
||||
checkoutInfo={checkoutInfo}
|
||||
currency={currencySign}
|
||||
currency={currency}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@ -287,11 +273,11 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps & DispatchProps> = ({
|
||||
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<OwnProps>(
|
||||
needCountry,
|
||||
needZip,
|
||||
error,
|
||||
globalDialogs: global.dialogs,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => {
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
shippingMethod,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const currencySign = getCurrencySign(currency);
|
||||
const checkoutInfo = useMemo(() => {
|
||||
return getCheckoutInfo(credentialsTitle, info, shippingMethod);
|
||||
}, [info, shippingMethod, credentialsTitle]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="PaymentModal PaymentModal-receipt"
|
||||
@ -87,7 +86,7 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
|
||||
title,
|
||||
}}
|
||||
checkoutInfo={checkoutInfo}
|
||||
currency={currencySign}
|
||||
currency={currency}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,7 +99,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { receipt } = global.payment;
|
||||
const {
|
||||
currency,
|
||||
prices: mapedPrices,
|
||||
prices,
|
||||
info,
|
||||
totalAmount,
|
||||
credentialsTitle,
|
||||
@ -113,7 +112,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
currency,
|
||||
prices: mapedPrices,
|
||||
prices,
|
||||
info,
|
||||
totalAmount,
|
||||
credentialsTitle,
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
currency,
|
||||
dispatch,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shippingOptions || state.shipping) {
|
||||
return;
|
||||
@ -36,9 +40,9 @@ const Shipping: FC<OwnProps> = ({
|
||||
|
||||
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 (
|
||||
<div className="Shipping">
|
||||
|
||||
@ -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'
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
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<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;
|
||||
}, {});
|
||||
const { message: description, code, param } = error;
|
||||
const { field, message } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code];
|
||||
|
||||
return { field, message, description};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
8
src/util/formatCurrency.ts
Normal file
8
src/util/formatCurrency.ts
Normal 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);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ApiError } from '../api/types';
|
||||
import { ApiError, ApiFieldError } from '../api/types';
|
||||
|
||||
const READABLE_ERROR_MESSAGES: Record<string, string> = {
|
||||
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',
|
||||
};
|
||||
|
||||
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) {
|
||||
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];
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user