Payments: Support tips and saving payment info, refactoring (#2097)
This commit is contained in:
parent
47032259a2
commit
9a26a8270a
@ -752,8 +752,8 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
|
||||
} = media;
|
||||
|
||||
return {
|
||||
text,
|
||||
title,
|
||||
text,
|
||||
photo: buildApiWebDocument(photo),
|
||||
receiptMsgId,
|
||||
amount: Number(totalAmount),
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiInvoice, ApiPaymentSavedInfo, ApiPremiumPromo, ApiPremiumSubscriptionOption,
|
||||
ApiPaymentForm, ApiReceipt, ApiLabeledPrice, ApiPaymentCredentials,
|
||||
} from '../../types';
|
||||
|
||||
import { buildApiDocument, buildApiMessageEntity, buildApiWebDocument } from './messages';
|
||||
@ -11,6 +12,7 @@ export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] |
|
||||
if (!shippingOptions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.values(shippingOptions).map((option) => {
|
||||
return {
|
||||
id: option.id,
|
||||
@ -26,7 +28,7 @@ export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] |
|
||||
});
|
||||
}
|
||||
|
||||
export function buildReceipt(receipt: GramJs.payments.PaymentReceipt) {
|
||||
export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiReceipt {
|
||||
const {
|
||||
invoice,
|
||||
info,
|
||||
@ -34,18 +36,19 @@ export function buildReceipt(receipt: GramJs.payments.PaymentReceipt) {
|
||||
currency,
|
||||
totalAmount,
|
||||
credentialsTitle,
|
||||
tipAmount,
|
||||
} = receipt;
|
||||
|
||||
const { shippingAddress, phone, name } = (info || {});
|
||||
|
||||
const { prices } = invoice;
|
||||
const mapedPrices = prices.map(({ label, amount }) => ({
|
||||
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
|
||||
label,
|
||||
amount: amount.toJSNumber(),
|
||||
}));
|
||||
|
||||
let shippingPrices;
|
||||
let shippingMethod;
|
||||
let shippingPrices: ApiLabeledPrice[] | undefined;
|
||||
let shippingMethod: string | undefined;
|
||||
|
||||
if (shipping) {
|
||||
shippingPrices = shipping.prices.map(({ label, amount }) => {
|
||||
@ -59,41 +62,43 @@ export function buildReceipt(receipt: GramJs.payments.PaymentReceipt) {
|
||||
|
||||
return {
|
||||
currency,
|
||||
prices: mapedPrices,
|
||||
prices: mappedPrices,
|
||||
info: { shippingAddress, phone, name },
|
||||
totalAmount: totalAmount.toJSNumber(),
|
||||
credentialsTitle,
|
||||
shippingPrices,
|
||||
shippingMethod,
|
||||
tipAmount: tipAmount ? tipAmount.toJSNumber() : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPaymentForm(form: GramJs.payments.PaymentForm) {
|
||||
export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPaymentForm {
|
||||
const {
|
||||
formId,
|
||||
canSaveCredentials,
|
||||
passwordMissing,
|
||||
passwordMissing: isPasswordMissing,
|
||||
providerId,
|
||||
nativeProvider,
|
||||
nativeParams,
|
||||
savedInfo,
|
||||
invoice,
|
||||
savedCredentials,
|
||||
} = form;
|
||||
|
||||
const {
|
||||
test,
|
||||
nameRequested,
|
||||
phoneRequested,
|
||||
emailRequested,
|
||||
shippingAddressRequested,
|
||||
flexible,
|
||||
phoneToProvider,
|
||||
emailToProvider,
|
||||
test: isTest,
|
||||
nameRequested: isNameRequested,
|
||||
phoneRequested: isPhoneRequested,
|
||||
emailRequested: isEmailRequested,
|
||||
shippingAddressRequested: isShippingAddressRequested,
|
||||
flexible: isFlexible,
|
||||
phoneToProvider: shouldSendPhoneToProvider,
|
||||
emailToProvider: shouldSendEmailToProvider,
|
||||
currency,
|
||||
prices,
|
||||
} = invoice;
|
||||
|
||||
const mappedPrices = prices.map(({ label, amount }) => ({
|
||||
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
|
||||
label,
|
||||
amount: amount.toJSNumber(),
|
||||
}));
|
||||
@ -107,30 +112,31 @@ export function buildPaymentForm(form: GramJs.payments.PaymentForm) {
|
||||
|
||||
return {
|
||||
canSaveCredentials,
|
||||
passwordMissing,
|
||||
isPasswordMissing,
|
||||
formId: String(formId),
|
||||
providerId: String(providerId),
|
||||
nativeProvider,
|
||||
savedInfo: cleanedInfo,
|
||||
invoice: {
|
||||
test,
|
||||
nameRequested,
|
||||
phoneRequested,
|
||||
emailRequested,
|
||||
shippingAddressRequested,
|
||||
flexible,
|
||||
phoneToProvider,
|
||||
emailToProvider,
|
||||
invoiceContainer: {
|
||||
isTest,
|
||||
isNameRequested,
|
||||
isPhoneRequested,
|
||||
isEmailRequested,
|
||||
isShippingAddressRequested,
|
||||
isFlexible,
|
||||
shouldSendPhoneToProvider,
|
||||
shouldSendEmailToProvider,
|
||||
currency,
|
||||
prices: mappedPrices,
|
||||
},
|
||||
nativeParams: {
|
||||
needCardholderName: nativeData.need_cardholder_name,
|
||||
needCountry: nativeData.need_country,
|
||||
needZip: nativeData.need_zip,
|
||||
publishableKey: nativeData.publishable_key,
|
||||
needCardholderName: Boolean(nativeData?.need_cardholder_name),
|
||||
needCountry: Boolean(nativeData?.need_country),
|
||||
needZip: Boolean(nativeData?.need_zip),
|
||||
publishableKey: nativeData?.publishable_key,
|
||||
publicToken: nativeData?.public_token,
|
||||
},
|
||||
...(savedCredentials && { savedCredentials: buildApiPaymentCredentials(savedCredentials) }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -139,7 +145,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI
|
||||
invoice, description: text, title, photo,
|
||||
} = form;
|
||||
const {
|
||||
test, currency, prices, recurring, recurringTermsUrl,
|
||||
test, currency, prices, recurring, recurringTermsUrl, maxTipAmount, suggestedTipAmounts,
|
||||
} = invoice;
|
||||
|
||||
const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0);
|
||||
@ -153,6 +159,8 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI
|
||||
isTest: test,
|
||||
isRecurring: recurring,
|
||||
recurringTermsUrl,
|
||||
maxTipAmount: maxTipAmount?.toJSNumber(),
|
||||
...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -184,3 +192,7 @@ function buildApiPremiumSubscriptionOption(option: GramJs.PremiumSubscriptionOpt
|
||||
months,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiPaymentCredentials(credentials: GramJs.PaymentSavedCredentialsCard[]): ApiPaymentCredentials[] {
|
||||
return credentials.map(({ id, title }) => ({ id, title }));
|
||||
}
|
||||
|
||||
@ -309,6 +309,10 @@ export function updateTwoFaSettings(params: TwoFaParams) {
|
||||
return client.updateTwoFaSettings(params);
|
||||
}
|
||||
|
||||
export function getTmpPassword(currentPassword: string, ttl?: number) {
|
||||
return client.getTmpPassword(currentPassword, ttl);
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser() {
|
||||
const userFull = await invokeRequest(new GramJs.users.GetFullUser({
|
||||
id: new GramJs.InputUserSelf(),
|
||||
|
||||
@ -74,7 +74,7 @@ export {
|
||||
} from './bots';
|
||||
|
||||
export {
|
||||
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo,
|
||||
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
|
||||
} from './payments';
|
||||
|
||||
export {
|
||||
|
||||
@ -3,14 +3,23 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { invokeRequest } from './client';
|
||||
import { buildInputInvoice, buildInputPeer, buildShippingInfo } from '../gramjsBuilders';
|
||||
import {
|
||||
buildShippingOptions, buildPaymentForm, buildReceipt, buildApiPremiumPromo, buildApiInvoiceFromForm,
|
||||
buildApiInvoiceFromForm,
|
||||
buildApiPremiumPromo,
|
||||
buildApiPaymentForm,
|
||||
buildApiReceipt,
|
||||
buildShippingOptions,
|
||||
} from '../apiBuilders/payments';
|
||||
import type {
|
||||
ApiChat, OnApiUpdate, ApiRequestInputInvoice,
|
||||
} from '../../types';
|
||||
import localDb from '../localDb';
|
||||
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
||||
import {
|
||||
addEntitiesWithPhotosToLocalDb,
|
||||
deserializeBytes,
|
||||
serializeBytes,
|
||||
} from '../helpers';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { getTemporaryPaymentPassword } from './twoFaSettings';
|
||||
|
||||
let onUpdate: OnApiUpdate;
|
||||
|
||||
@ -56,22 +65,35 @@ export async function sendPaymentForm({
|
||||
requestedInfoId,
|
||||
shippingOptionId,
|
||||
credentials,
|
||||
savedCredentialId,
|
||||
temporaryPassword,
|
||||
tipAmount,
|
||||
}: {
|
||||
inputInvoice: ApiRequestInputInvoice;
|
||||
formId: string;
|
||||
credentials: any;
|
||||
requestedInfoId?: string;
|
||||
shippingOptionId?: string;
|
||||
savedCredentialId?: string;
|
||||
temporaryPassword?: string;
|
||||
tipAmount?: number;
|
||||
}) {
|
||||
const inputCredentials = temporaryPassword && savedCredentialId
|
||||
? new GramJs.InputPaymentCredentialsSaved({
|
||||
id: savedCredentialId,
|
||||
tmpPassword: deserializeBytes(temporaryPassword),
|
||||
})
|
||||
: new GramJs.InputPaymentCredentials({
|
||||
save: credentials.save,
|
||||
data: new GramJs.DataJSON({ data: JSON.stringify(credentials.data) }),
|
||||
});
|
||||
const result = await invokeRequest(new GramJs.payments.SendPaymentForm({
|
||||
formId: BigInt(formId),
|
||||
invoice: buildInputInvoice(inputInvoice),
|
||||
requestedInfoId,
|
||||
shippingOptionId,
|
||||
credentials: new GramJs.InputPaymentCredentials({
|
||||
save: credentials.save,
|
||||
data: new GramJs.DataJSON({ data: JSON.stringify(credentials.data) }),
|
||||
}),
|
||||
credentials: inputCredentials,
|
||||
...(tipAmount && { tipAmount: BigInt(tipAmount) }),
|
||||
}));
|
||||
|
||||
if (result instanceof GramJs.payments.PaymentVerificationNeeded) {
|
||||
@ -99,8 +121,10 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
|
||||
localDb.webDocuments[result.photo.url] = result.photo;
|
||||
}
|
||||
|
||||
addEntitiesWithPhotosToLocalDb(result.users);
|
||||
|
||||
return {
|
||||
form: buildPaymentForm(result),
|
||||
form: buildApiPaymentForm(result),
|
||||
invoice: buildApiInvoiceFromForm(result),
|
||||
};
|
||||
}
|
||||
@ -110,11 +134,12 @@ export async function getReceipt(chat: ApiChat, msgId: number) {
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildReceipt(result);
|
||||
return buildApiReceipt(result);
|
||||
}
|
||||
|
||||
export async function fetchPremiumPromo() {
|
||||
@ -135,3 +160,20 @@ export async function fetchPremiumPromo() {
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchTemporaryPaymentPassword(password: string) {
|
||||
const result = await getTemporaryPaymentPassword(password);
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
value: serializeBytes(result.tmpPassword),
|
||||
validUntil: result.validUntil,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { Api as GramJs, errors } from '../../../lib/gramjs';
|
||||
import type { OnApiUpdate } from '../../types';
|
||||
|
||||
import { DEBUG } from '../../../config';
|
||||
import { invokeRequest, updateTwoFaSettings } from './client';
|
||||
import { invokeRequest, updateTwoFaSettings, getTmpPassword } from './client';
|
||||
|
||||
const ApiErrors: { [k: string]: string } = {
|
||||
EMAIL_UNCONFIRMED: 'Email unconfirmed',
|
||||
@ -48,6 +48,10 @@ function onRequestEmailCode(length: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getTemporaryPaymentPassword(password: string, ttl?: number) {
|
||||
return getTmpPassword(password, ttl);
|
||||
}
|
||||
|
||||
export async function checkPassword(currentPassword: string) {
|
||||
try {
|
||||
await updateTwoFaSettings({ isCheckPassword: true, currentPassword });
|
||||
|
||||
@ -187,6 +187,13 @@ export interface ApiInvoice {
|
||||
isTest?: boolean;
|
||||
isRecurring?: boolean;
|
||||
recurringTermsUrl?: string;
|
||||
maxTipAmount?: number;
|
||||
suggestedTipAmounts?: number[];
|
||||
}
|
||||
|
||||
export interface ApiPaymentCredentials {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ApiGeoPoint {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { ApiDocument, ApiMessageEntity } from './messages';
|
||||
import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiInvoiceContainer } from '../../types';
|
||||
|
||||
export interface ApiShippingAddress {
|
||||
streetLine1: string;
|
||||
@ -18,22 +20,13 @@ export interface ApiPaymentSavedInfo {
|
||||
|
||||
export interface ApiPaymentForm {
|
||||
canSaveCredentials?: boolean;
|
||||
passwordMissing?: boolean;
|
||||
isPasswordMissing?: boolean;
|
||||
formId: string;
|
||||
providerId: string;
|
||||
nativeProvider?: string;
|
||||
savedInfo: any;
|
||||
invoice: {
|
||||
test?: boolean;
|
||||
nameRequested?: boolean;
|
||||
phoneRequested?: boolean;
|
||||
emailRequested?: boolean;
|
||||
shippingAddressRequested?: boolean;
|
||||
flexible?: boolean;
|
||||
phoneToProvider?: boolean;
|
||||
emailToProvider?: boolean;
|
||||
currency?: string;
|
||||
prices?: ApiLabeledPrice[];
|
||||
};
|
||||
savedInfo?: ApiPaymentSavedInfo;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
invoiceContainer: ApiInvoiceContainer;
|
||||
nativeParams: ApiPaymentFormNativeParams;
|
||||
}
|
||||
|
||||
@ -51,6 +44,9 @@ export interface ApiLabeledPrice {
|
||||
}
|
||||
|
||||
export interface ApiReceipt {
|
||||
photo?: ApiWebDocument;
|
||||
text?: string;
|
||||
title?: string;
|
||||
currency: string;
|
||||
prices: ApiLabeledPrice[];
|
||||
info?: {
|
||||
@ -58,6 +54,7 @@ export interface ApiReceipt {
|
||||
phone?: string;
|
||||
name?: string;
|
||||
};
|
||||
tipAmount: number;
|
||||
totalAmount: number;
|
||||
credentialsTitle: string;
|
||||
shippingPrices?: ApiLabeledPrice[];
|
||||
|
||||
@ -7,6 +7,7 @@ import React, {
|
||||
import { MIN_PASSWORD_LENGTH } from '../../config';
|
||||
import { IS_TOUCH_ENV, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useTimeout from '../../hooks/useTimeout';
|
||||
|
||||
@ -17,6 +18,7 @@ type OwnProps = {
|
||||
error?: string;
|
||||
hint?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
isLoading?: boolean;
|
||||
shouldDisablePasswordManager?: boolean;
|
||||
shouldShowSubmit?: boolean;
|
||||
@ -26,7 +28,7 @@ type OwnProps = {
|
||||
noRipple?: boolean;
|
||||
onChangePasswordVisibility: (state: boolean) => void;
|
||||
onInputChange?: (password: string) => void;
|
||||
onSubmit: (password: string) => void;
|
||||
onSubmit?: (password: string) => void;
|
||||
};
|
||||
|
||||
const FOCUS_DELAY_TIMEOUT_MS = IS_SINGLE_COLUMN_LAYOUT ? 550 : 400;
|
||||
@ -38,6 +40,7 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
hint,
|
||||
placeholder = 'Password',
|
||||
submitLabel = 'Next',
|
||||
description,
|
||||
shouldShowSubmit,
|
||||
shouldResetValue,
|
||||
shouldDisablePasswordManager = false,
|
||||
@ -100,7 +103,7 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
if (canSubmit) {
|
||||
onSubmit(password);
|
||||
onSubmit!(password);
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +120,7 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form action="" onSubmit={handleSubmit} autoComplete="off">
|
||||
<form action="" onSubmit={onSubmit ? handleSubmit : stopEvent} autoComplete="off">
|
||||
<div
|
||||
className={buildClassName('input-group password-input', password && 'touched', error && 'error')}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
@ -145,7 +148,8 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
<i className={isPasswordVisible ? 'icon-eye' : 'icon-eye-closed'} />
|
||||
</div>
|
||||
</div>
|
||||
{(canSubmit || shouldShowSubmit) && (
|
||||
{description && <p className="description">{description}</p>}
|
||||
{onSubmit && (canSubmit || shouldShowSubmit) && (
|
||||
<Button type="submit" ripple={!noRipple} isLoading={isLoading} disabled={!canSubmit}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
|
||||
@ -59,29 +59,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-info {
|
||||
border-top: 1px var(--color-borders) solid;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.checkout-info-item {
|
||||
.tipsList {
|
||||
display: flex;
|
||||
padding: 0.75rem 0.5rem 1rem;
|
||||
text-align: left;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem
|
||||
}
|
||||
|
||||
.checkout-info-item-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-right: 2rem;
|
||||
width: 1.5rem;
|
||||
.tipsItem {
|
||||
border-radius: 1.375rem;
|
||||
padding: 0 0.75rem;
|
||||
height: 2.5rem;
|
||||
min-width: 5rem;
|
||||
line-height: 2.5rem;
|
||||
text-align: center;
|
||||
background: var(--color-primary-opacity);
|
||||
color: var(--color-primary);
|
||||
transition: background-color 200ms, color 200ms;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--color-primary-opacity-hover);
|
||||
}
|
||||
|
||||
&_active {
|
||||
color: var(--color-white);
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
:global(.theme-dark) & {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-info {
|
||||
border-top: 0.0625rem var(--color-borders) solid;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.provider {
|
||||
float: left;
|
||||
background: no-repeat center;
|
||||
background-size: 2rem;
|
||||
border-radius: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.provider.stripe {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
|
||||
import type { LangCode, Price } from '../../types';
|
||||
import type { ApiChat, ApiWebDocument } from '../../api/types';
|
||||
import type { ApiChat, ApiInvoice, ApiPaymentCredentials } from '../../api/types';
|
||||
|
||||
import { PaymentStep } from '../../types';
|
||||
import { getWebDocumentHash } from '../../global/helpers';
|
||||
import { formatCurrency } from '../../util/formatCurrency';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -15,18 +18,13 @@ import useMedia from '../../hooks/useMedia';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Skeleton from '../ui/Skeleton';
|
||||
import SafeLink from '../common/SafeLink';
|
||||
import ListItem from '../ui/ListItem';
|
||||
|
||||
import styles from './Checkout.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
chat?: ApiChat;
|
||||
invoiceContent?: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
photo?: ApiWebDocument;
|
||||
isRecurring?: boolean;
|
||||
recurringTermsUrl?: string;
|
||||
};
|
||||
invoice?: ApiInvoice;
|
||||
checkoutInfo?: {
|
||||
paymentMethod?: string;
|
||||
paymentProvider?: string;
|
||||
@ -37,28 +35,41 @@ export type OwnProps = {
|
||||
};
|
||||
prices?: Price[];
|
||||
totalPrice?: number;
|
||||
needAddress?: boolean;
|
||||
hasShippingOptions?: boolean;
|
||||
tipAmount?: number;
|
||||
shippingPrices?: Price[];
|
||||
currency: string;
|
||||
isTosAccepted?: boolean;
|
||||
dispatch?: FormEditDispatch;
|
||||
onAcceptTos?: (isAccepted: boolean) => void;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
};
|
||||
|
||||
const Checkout: FC<OwnProps> = ({
|
||||
chat,
|
||||
invoiceContent,
|
||||
invoice,
|
||||
prices,
|
||||
shippingPrices,
|
||||
checkoutInfo,
|
||||
currency,
|
||||
totalPrice,
|
||||
isTosAccepted,
|
||||
dispatch,
|
||||
onAcceptTos,
|
||||
tipAmount,
|
||||
needAddress,
|
||||
hasShippingOptions,
|
||||
savedCredentials,
|
||||
}) => {
|
||||
const { setPaymentStep } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const isInteractive = Boolean(dispatch);
|
||||
|
||||
const {
|
||||
photo, title, text, isRecurring, recurringTermsUrl,
|
||||
} = invoiceContent || {};
|
||||
photo, title, text, isRecurring, recurringTermsUrl, suggestedTipAmounts, maxTipAmount,
|
||||
} = invoice || {};
|
||||
const {
|
||||
paymentMethod,
|
||||
paymentProvider,
|
||||
@ -70,6 +81,48 @@ const Checkout: FC<OwnProps> = ({
|
||||
|
||||
const photoUrl = useMedia(getWebDocumentHash(photo));
|
||||
|
||||
const handleTipsClick = useCallback((tips: number) => {
|
||||
dispatch!({ type: 'setTipAmount', payload: maxTipAmount ? Math.min(tips, maxTipAmount) : tips });
|
||||
}, [dispatch, maxTipAmount]);
|
||||
|
||||
const handlePaymentMethodClick = useCallback(() => {
|
||||
setPaymentStep({ step: savedCredentials?.length ? PaymentStep.SavedPayments : PaymentStep.PaymentInfo });
|
||||
}, [savedCredentials?.length, setPaymentStep]);
|
||||
|
||||
const handleShippingAddressClick = useCallback(() => {
|
||||
setPaymentStep({ step: PaymentStep.ShippingInfo });
|
||||
}, [setPaymentStep]);
|
||||
|
||||
const handleShippingMethodClick = useCallback(() => {
|
||||
setPaymentStep({ step: PaymentStep.Shipping });
|
||||
}, [setPaymentStep]);
|
||||
|
||||
function renderTips() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.priceInfoItem}>
|
||||
<div className={styles.priceInfoItemTitle}>
|
||||
{title}
|
||||
</div>
|
||||
<div>
|
||||
{formatCurrency(tipAmount!, currency, lang.code)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.tipsList}>
|
||||
{suggestedTipAmounts!.map((tip) => (
|
||||
<div
|
||||
key={tip}
|
||||
className={buildClassName(styles.tipsItem, tip === tipAmount && styles.tipsItem_active)}
|
||||
onClick={dispatch ? () => handleTipsClick(tip === tipAmount ? 0 : tip) : undefined}
|
||||
>
|
||||
{formatCurrency(tip, currency, lang.code, true)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTosLink(url: string, isRtl?: boolean) {
|
||||
const langString = lang('PaymentCheckoutAcceptRecurrent', chat?.title);
|
||||
const langStringSplit = langString.split('*');
|
||||
@ -125,27 +178,53 @@ const Checkout: FC<OwnProps> = ({
|
||||
{shippingPrices && shippingPrices.map((item) => (
|
||||
renderPaymentItem(lang.code, item.label, item.amount, currency)
|
||||
))}
|
||||
{suggestedTipAmounts && suggestedTipAmounts.length > 0 && renderTips()}
|
||||
{totalPrice !== undefined && (
|
||||
renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, currency, true)
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.invoiceInfo}>
|
||||
{paymentMethod && renderCheckoutItem('icon-card', paymentMethod, lang('PaymentCheckoutMethod'))}
|
||||
{paymentProvider && renderCheckoutItem(
|
||||
buildClassName(styles.provider, styles[paymentProvider.toLowerCase()]),
|
||||
paymentProvider,
|
||||
lang('PaymentCheckoutProvider'),
|
||||
)}
|
||||
{shippingAddress && renderCheckoutItem('icon-location', shippingAddress, lang('PaymentShippingAddress'))}
|
||||
{name && renderCheckoutItem('icon-user', name, lang('PaymentCheckoutName'))}
|
||||
{phone && renderCheckoutItem('icon-phone', phone, lang('PaymentCheckoutPhoneNumber'))}
|
||||
{shippingMethod && renderCheckoutItem('icon-truck', shippingMethod, lang('PaymentCheckoutShippingMethod'))}
|
||||
{renderCheckoutItem({
|
||||
title: paymentMethod || savedCredentials?.[0].title,
|
||||
label: lang('PaymentCheckoutMethod'),
|
||||
icon: 'card',
|
||||
onClick: isInteractive ? handlePaymentMethodClick : undefined,
|
||||
})}
|
||||
{paymentProvider && renderCheckoutItem({
|
||||
title: paymentProvider,
|
||||
label: lang('PaymentCheckoutProvider'),
|
||||
customIcon: buildClassName(styles.provider, styles[paymentProvider.toLowerCase()]),
|
||||
})}
|
||||
{(needAddress || !isInteractive) && renderCheckoutItem({
|
||||
title: shippingAddress,
|
||||
label: lang('PaymentShippingAddress'),
|
||||
icon: 'location',
|
||||
onClick: isInteractive ? handleShippingAddressClick : undefined,
|
||||
})}
|
||||
{name && renderCheckoutItem({
|
||||
title: name,
|
||||
label: lang('PaymentCheckoutName'),
|
||||
icon: 'user',
|
||||
})}
|
||||
{phone && renderCheckoutItem({
|
||||
title: phone,
|
||||
label: lang('PaymentCheckoutPhoneNumber'),
|
||||
icon: 'phone',
|
||||
})}
|
||||
{(hasShippingOptions || !isInteractive) && renderCheckoutItem({
|
||||
title: shippingMethod,
|
||||
label: lang('PaymentCheckoutShippingMethod'),
|
||||
icon: 'truck',
|
||||
onClick: isInteractive ? handleShippingMethodClick : undefined,
|
||||
})}
|
||||
{isRecurring && renderTos(recurringTermsUrl!)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Checkout);
|
||||
|
||||
function renderPaymentItem(
|
||||
langCode: LangCode | undefined, title: string, value: number, currency: string, main = false,
|
||||
) {
|
||||
@ -161,20 +240,35 @@ function renderPaymentItem(
|
||||
);
|
||||
}
|
||||
|
||||
function renderCheckoutItem(icon: string, title: string, data: string) {
|
||||
function renderCheckoutItem({
|
||||
title,
|
||||
label,
|
||||
icon,
|
||||
customIcon,
|
||||
onClick,
|
||||
}: {
|
||||
title : string | undefined;
|
||||
label: string | undefined;
|
||||
icon?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
customIcon?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.checkoutInfoItem}>
|
||||
<i className={buildClassName(icon, styles.checkoutInfoItemIcon)}> </i>
|
||||
<div className={styles.checkoutInfoItemInfo}>
|
||||
<div className={styles.checkoutInfoItemInfoTitle}>
|
||||
{title}
|
||||
</div>
|
||||
<p className={styles.checkoutInfoItemInfoData}>
|
||||
{data}
|
||||
</p>
|
||||
<ListItem
|
||||
multiline={Boolean(title && label !== title)}
|
||||
icon={icon}
|
||||
inactive={!onClick}
|
||||
onClick={onClick}
|
||||
>
|
||||
{customIcon && <i className={customIcon} />}
|
||||
<div className={styles.checkoutInfoItemInfoTitle}>
|
||||
{title || label}
|
||||
</div>
|
||||
</div>
|
||||
{title && label !== title && (
|
||||
<p className={styles.checkoutInfoItemInfoData}>
|
||||
{label}
|
||||
</p>
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Checkout);
|
||||
|
||||
70
src/components/payment/PasswordConfirm.tsx
Normal file
70
src/components/payment/PasswordConfirm.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { memo, useMemo, useState } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiPaymentCredentials } from '../../api/types';
|
||||
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import PasswordMonkey from '../common/PasswordMonkey';
|
||||
import PasswordForm from '../common/PasswordForm';
|
||||
|
||||
interface OwnProps {
|
||||
isActive?: boolean;
|
||||
state: FormState;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
onPasswordChange: (password: string) => void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
error?: string;
|
||||
passwordHint?: string;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
}
|
||||
|
||||
const PasswordConfirm: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
error,
|
||||
state,
|
||||
savedCredentials,
|
||||
passwordHint,
|
||||
onPasswordChange,
|
||||
}) => {
|
||||
const { clearPaymentError } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [shouldShowPassword, setShouldShowPassword] = useState(false);
|
||||
const cardName = useMemo(() => {
|
||||
return savedCredentials?.length && state.savedCredentialId
|
||||
? savedCredentials.find(({ id }) => id === state.savedCredentialId)?.title
|
||||
: undefined;
|
||||
}, [savedCredentials, state.savedCredentialId]);
|
||||
|
||||
return (
|
||||
<div className="PaymentInfo">
|
||||
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
|
||||
|
||||
<PasswordForm
|
||||
error={error ? lang(error) : undefined}
|
||||
hint={passwordHint}
|
||||
description={lang('PaymentConfirmationMessage', cardName)}
|
||||
placeholder={lang('Password')}
|
||||
clearError={clearPaymentError}
|
||||
shouldShowSubmit={false}
|
||||
shouldResetValue={isActive}
|
||||
isPasswordVisible={shouldShowPassword}
|
||||
onChangePasswordVisibility={setShouldShowPassword}
|
||||
onInputChange={onPasswordChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
return {
|
||||
error: global.payment.error?.message,
|
||||
passwordHint: global.twoFaSettings.hint,
|
||||
savedCredentials: global.payment.savedCredentials,
|
||||
};
|
||||
})(PasswordConfirm));
|
||||
@ -17,4 +17,11 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: -0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,14 +153,16 @@ const PaymentInfo: FC<OwnProps> = ({
|
||||
error={formErrors.billingZip}
|
||||
/>
|
||||
)}
|
||||
{ canSaveCredentials && (
|
||||
<Checkbox
|
||||
label={lang('PaymentCardSavePaymentInformation')}
|
||||
checked={state.saveCredentials}
|
||||
tabIndex={0}
|
||||
onChange={handleChangeSaveCredentials}
|
||||
/>
|
||||
) }
|
||||
<Checkbox
|
||||
label={lang('PaymentCardSavePaymentInformation')}
|
||||
checked={canSaveCredentials ? state.saveCredentials : false}
|
||||
tabIndex={0}
|
||||
onChange={handleChangeSaveCredentials}
|
||||
disabled={!canSaveCredentials}
|
||||
/>
|
||||
<p className="description">
|
||||
{lang(canSaveCredentials ? 'Checkout.NewCard.SaveInfoHelp' : 'Checkout.2FA.Text')}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -17,12 +17,11 @@ $modalHeaderAndFooterHeight: 8.375rem;
|
||||
border-top-left-radius: var(--border-radius-default-small);
|
||||
border-top-right-radius: var(--border-radius-default-small);
|
||||
width: 100%;
|
||||
padding: 0.25rem 1rem;
|
||||
padding: 0.5rem 1rem 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
background: var(--color-background);
|
||||
border-bottom: 1px var(--color-borders) solid;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
@ -33,7 +32,7 @@ $modalHeaderAndFooterHeight: 8.375rem;
|
||||
}
|
||||
|
||||
.Transition {
|
||||
height: min(25rem, 60vh);
|
||||
height: min(27rem, 60vh);
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
@ -51,6 +50,7 @@ $modalHeaderAndFooterHeight: 8.375rem;
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
@ -64,7 +64,7 @@ $modalHeaderAndFooterHeight: 8.375rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--color-background);
|
||||
border-top: 1px var(--color-borders) solid;
|
||||
border-top: 0.0625rem var(--color-borders) solid;
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
|
||||
@ -5,17 +5,19 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { ApiChat, ApiCountry } from '../../api/types';
|
||||
import type { ShippingOption, Price } from '../../types';
|
||||
import { PaymentStep } from '../../types';
|
||||
import type { ApiChat, ApiCountry, ApiPaymentCredentials } from '../../api/types';
|
||||
import type { Price, ShippingOption } from '../../types';
|
||||
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
|
||||
|
||||
import { PaymentStep } from '../../types';
|
||||
import { selectChat } from '../../global/selectors';
|
||||
import { formatCurrency } from '../../util/formatCurrency';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { detectCardTypeText } from '../common/helpers/detectCardType';
|
||||
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
|
||||
import usePaymentReducer from '../../hooks/reducers/usePaymentReducer';
|
||||
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import usePaymentReducer from '../../hooks/reducers/usePaymentReducer';
|
||||
|
||||
import ShippingInfo from './ShippingInfo';
|
||||
import Shipping from './Shipping';
|
||||
@ -26,6 +28,8 @@ import Modal from '../ui/Modal';
|
||||
import Transition from '../ui/Transition';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import ConfirmPayment from './ConfirmPayment';
|
||||
import SavedPaymentCredentials from './SavedPaymentCredentials';
|
||||
import PasswordConfirm from './PasswordConfirm';
|
||||
|
||||
import './PaymentModal.scss';
|
||||
|
||||
@ -40,13 +44,12 @@ export type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
nameRequested?: boolean;
|
||||
shippingAddressRequested?: boolean;
|
||||
phoneRequested?: boolean;
|
||||
emailRequested?: boolean;
|
||||
flexible?: boolean;
|
||||
phoneToProvider?: boolean;
|
||||
emailToProvider?: boolean;
|
||||
isNameRequested?: boolean;
|
||||
isShippingAddressRequested?: boolean;
|
||||
isPhoneRequested?: boolean;
|
||||
isEmailRequested?: boolean;
|
||||
shouldSendPhoneToProvider?: boolean;
|
||||
shouldSendEmailToProvider?: boolean;
|
||||
currency?: string;
|
||||
prices?: Price[];
|
||||
isProviderError: boolean;
|
||||
@ -55,14 +58,21 @@ type StateProps = {
|
||||
needZip?: boolean;
|
||||
confirmPaymentUrl?: string;
|
||||
countryList: ApiCountry[];
|
||||
hasShippingOptions: boolean;
|
||||
requestId?: string;
|
||||
smartGlocalToken?: string;
|
||||
stripeId?: string;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
passwordValidUntil?: number;
|
||||
};
|
||||
|
||||
type GlobalStateProps = Pick<GlobalState['payment'], (
|
||||
'step' | 'shippingOptions' |
|
||||
'savedInfo' | 'canSaveCredentials' | 'nativeProvider' | 'passwordMissing' | 'invoiceContent' |
|
||||
'error'
|
||||
'savedInfo' | 'canSaveCredentials' | 'nativeProvider' | 'passwordMissing' | 'invoice' | 'error'
|
||||
)>;
|
||||
|
||||
const NETWORK_REQUEST_TIMEOUT_S = 3;
|
||||
|
||||
const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@ -71,16 +81,16 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
shippingOptions,
|
||||
savedInfo,
|
||||
canSaveCredentials,
|
||||
nameRequested,
|
||||
shippingAddressRequested,
|
||||
phoneRequested,
|
||||
emailRequested,
|
||||
phoneToProvider,
|
||||
emailToProvider,
|
||||
isNameRequested,
|
||||
isShippingAddressRequested,
|
||||
isPhoneRequested,
|
||||
isEmailRequested,
|
||||
shouldSendPhoneToProvider,
|
||||
shouldSendEmailToProvider,
|
||||
currency,
|
||||
passwordMissing,
|
||||
isProviderError,
|
||||
invoiceContent,
|
||||
invoice,
|
||||
nativeProvider,
|
||||
prices,
|
||||
needCardholderName,
|
||||
@ -89,23 +99,47 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
confirmPaymentUrl,
|
||||
error,
|
||||
countryList,
|
||||
hasShippingOptions,
|
||||
requestId,
|
||||
smartGlocalToken,
|
||||
stripeId,
|
||||
savedCredentials,
|
||||
passwordValidUntil,
|
||||
}) => {
|
||||
const {
|
||||
loadPasswordInfo,
|
||||
validateRequestedInfo,
|
||||
sendPaymentForm,
|
||||
setPaymentStep,
|
||||
sendCredentialsInfo,
|
||||
clearPaymentError,
|
||||
validatePaymentPassword,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
||||
const [paymentState, paymentDispatch] = usePaymentReducer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTosAccepted, setIsTosAccepted] = useState(false);
|
||||
const lang = useLang();
|
||||
const [twoFaPassword, setTwoFaPassword] = useState('');
|
||||
|
||||
const canRenderFooter = step !== PaymentStep.ConfirmPayment;
|
||||
|
||||
const setStep = useCallback((nextStep) => {
|
||||
setPaymentStep({ step: nextStep });
|
||||
}, [setPaymentStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step || error) {
|
||||
if (isOpen) {
|
||||
setTwoFaPassword('');
|
||||
loadPasswordInfo();
|
||||
openModal();
|
||||
}
|
||||
}, [isOpen, loadPasswordInfo, openModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== undefined || error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [step, error]);
|
||||
@ -148,6 +182,15 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
}
|
||||
}, [savedInfo, paymentDispatch, countryList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (savedCredentials?.length) {
|
||||
paymentDispatch({
|
||||
type: 'changeSavedCredentialId',
|
||||
payload: savedCredentials[0].id,
|
||||
});
|
||||
}
|
||||
}, [paymentDispatch, savedCredentials]);
|
||||
|
||||
const handleErrorModalClose = useCallback(() => {
|
||||
clearPaymentError();
|
||||
}, [clearPaymentError]);
|
||||
@ -157,8 +200,8 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getTotalPrice(prices, shippingOptions, paymentState.shipping);
|
||||
}, [step, paymentState.shipping, prices, shippingOptions]);
|
||||
return getTotalPrice(prices, shippingOptions, paymentState.shipping, paymentState.tipAmount);
|
||||
}, [step, prices, shippingOptions, paymentState.shipping, paymentState.tipAmount]);
|
||||
|
||||
const checkoutInfo = useMemo(() => {
|
||||
if (step !== PaymentStep.Checkout) {
|
||||
@ -167,6 +210,10 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
return getCheckoutInfo(paymentState, shippingOptions, nativeProvider || '');
|
||||
}, [step, paymentState, shippingOptions, nativeProvider]);
|
||||
|
||||
const handleNewCardClick = useCallback(() => {
|
||||
setStep(PaymentStep.PaymentInfo);
|
||||
}, [setStep]);
|
||||
|
||||
function renderError() {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
@ -191,25 +238,43 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
|
||||
function renderModalContent(currentStep: PaymentStep) {
|
||||
switch (currentStep) {
|
||||
case PaymentStep.ShippingInfo:
|
||||
case PaymentStep.Checkout:
|
||||
return (
|
||||
<ShippingInfo
|
||||
state={paymentState}
|
||||
<Checkout
|
||||
chat={chat}
|
||||
prices={prices}
|
||||
dispatch={paymentDispatch}
|
||||
needAddress={Boolean(shippingAddressRequested)}
|
||||
needEmail={Boolean(emailRequested || emailToProvider)}
|
||||
needPhone={Boolean(phoneRequested || phoneToProvider)}
|
||||
needName={Boolean(nameRequested)}
|
||||
countryList={countryList}
|
||||
shippingPrices={paymentState.shipping && shippingOptions
|
||||
? getShippingPrices(shippingOptions, paymentState.shipping)
|
||||
: undefined}
|
||||
totalPrice={totalPrice}
|
||||
invoice={invoice}
|
||||
checkoutInfo={checkoutInfo}
|
||||
currency={currency!}
|
||||
hasShippingOptions={hasShippingOptions}
|
||||
tipAmount={paymentState.tipAmount}
|
||||
needAddress={Boolean(isShippingAddressRequested)}
|
||||
savedCredentials={savedCredentials}
|
||||
isTosAccepted={isTosAccepted}
|
||||
onAcceptTos={setIsTosAccepted}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.Shipping:
|
||||
case PaymentStep.SavedPayments:
|
||||
return (
|
||||
<Shipping
|
||||
<SavedPaymentCredentials
|
||||
state={paymentState}
|
||||
savedCredentials={savedCredentials}
|
||||
dispatch={paymentDispatch}
|
||||
shippingOptions={shippingOptions || []}
|
||||
currency={currency!}
|
||||
onNewCardClick={handleNewCardClick}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.ConfirmPassword:
|
||||
return (
|
||||
<PasswordConfirm
|
||||
state={paymentState}
|
||||
savedCredentials={savedCredentials}
|
||||
onPasswordChange={setTwoFaPassword}
|
||||
isActive={currentStep === step}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.PaymentInfo:
|
||||
@ -224,20 +289,25 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
countryList={countryList}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.Checkout:
|
||||
case PaymentStep.ShippingInfo:
|
||||
return (
|
||||
<Checkout
|
||||
chat={chat}
|
||||
prices={prices}
|
||||
shippingPrices={paymentState.shipping && shippingOptions
|
||||
? getShippingPrices(shippingOptions, paymentState.shipping)
|
||||
: undefined}
|
||||
totalPrice={totalPrice}
|
||||
invoiceContent={invoiceContent}
|
||||
checkoutInfo={checkoutInfo}
|
||||
<ShippingInfo
|
||||
state={paymentState}
|
||||
dispatch={paymentDispatch}
|
||||
needAddress={Boolean(isShippingAddressRequested)}
|
||||
needEmail={Boolean(isEmailRequested || shouldSendEmailToProvider)}
|
||||
needPhone={Boolean(isPhoneRequested || shouldSendPhoneToProvider)}
|
||||
needName={Boolean(isNameRequested)}
|
||||
countryList={countryList}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.Shipping:
|
||||
return (
|
||||
<Shipping
|
||||
state={paymentState}
|
||||
dispatch={paymentDispatch}
|
||||
shippingOptions={shippingOptions || []}
|
||||
currency={currency!}
|
||||
isTosAccepted={isTosAccepted}
|
||||
onAcceptTos={setIsTosAccepted}
|
||||
/>
|
||||
);
|
||||
case PaymentStep.ConfirmPayment:
|
||||
@ -268,48 +338,126 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
sendPaymentForm({
|
||||
shippingOptionId: paymentState.shipping,
|
||||
saveCredentials: paymentState.saveCredentials,
|
||||
savedCredentialId: paymentState.savedCredentialId,
|
||||
tipAmount: paymentState.tipAmount,
|
||||
});
|
||||
}, [sendPaymentForm, paymentState]);
|
||||
|
||||
const setStep = useCallback((nextStep) => {
|
||||
setPaymentStep({ step: nextStep });
|
||||
}, [setPaymentStep]);
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
switch (step) {
|
||||
case PaymentStep.ShippingInfo:
|
||||
setIsLoading(true);
|
||||
validateRequest();
|
||||
break;
|
||||
|
||||
case PaymentStep.Shipping:
|
||||
setStep(PaymentStep.PaymentInfo);
|
||||
setStep(PaymentStep.Checkout);
|
||||
break;
|
||||
|
||||
case PaymentStep.SavedPayments:
|
||||
setStep(PaymentStep.ConfirmPassword);
|
||||
break;
|
||||
|
||||
case PaymentStep.ConfirmPassword:
|
||||
if (twoFaPassword === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
validatePaymentPassword({ password: twoFaPassword });
|
||||
break;
|
||||
|
||||
case PaymentStep.PaymentInfo:
|
||||
setIsLoading(true);
|
||||
sendCredentials();
|
||||
paymentDispatch({ type: 'changeSavedCredentialId', payload: '' });
|
||||
break;
|
||||
case PaymentStep.Checkout:
|
||||
|
||||
case PaymentStep.Checkout: {
|
||||
if (savedInfo && !requestId && !paymentState.shipping) {
|
||||
setIsLoading(true);
|
||||
validateRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
paymentState.savedCredentialId
|
||||
&& (!passwordValidUntil || passwordValidUntil <= (Date.now() / 1000 - NETWORK_REQUEST_TIMEOUT_S))
|
||||
) {
|
||||
setStep(PaymentStep.ConfirmPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!paymentState.savedCredentialId
|
||||
&& (
|
||||
(nativeProvider === DEFAULT_PROVIDER && !stripeId)
|
||||
|| (nativeProvider === DONATE_PROVIDER && !smartGlocalToken)
|
||||
)
|
||||
) {
|
||||
setStep(PaymentStep.PaymentInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const { phone, email, fullName } = paymentState;
|
||||
const shouldFillRequestedData = (isEmailRequested && !email)
|
||||
|| (isPhoneRequested && !phone)
|
||||
|| (isNameRequested && !fullName);
|
||||
|
||||
if ((isShippingAddressRequested && !requestId) || shouldFillRequestedData) {
|
||||
setStep(PaymentStep.ShippingInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isShippingAddressRequested && !paymentState.shipping) {
|
||||
setStep(PaymentStep.Shipping);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
sendForm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [step, validateRequest, setStep, sendCredentials, sendForm]);
|
||||
}, [
|
||||
isEmailRequested, isNameRequested, isPhoneRequested, isShippingAddressRequested, nativeProvider, passwordValidUntil,
|
||||
paymentDispatch, paymentState, requestId, savedInfo, sendCredentials, sendForm, setStep, smartGlocalToken, step,
|
||||
stripeId, twoFaPassword, validatePaymentPassword, validateRequest,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return step === PaymentStep.ConfirmPassword
|
||||
? captureKeyboardListeners({ onEnter: handleButtonClick })
|
||||
: undefined;
|
||||
},
|
||||
[handleButtonClick, step]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
paymentDispatch({
|
||||
type: 'resetState',
|
||||
});
|
||||
setIsTosAccepted(false);
|
||||
}, [paymentDispatch]);
|
||||
onClose();
|
||||
}, [onClose, paymentDispatch]);
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
setStep(step === PaymentStep.ConfirmPassword ? PaymentStep.SavedPayments : PaymentStep.Checkout);
|
||||
}, [setStep, step]);
|
||||
|
||||
const modalHeader = useMemo(() => {
|
||||
switch (step) {
|
||||
case PaymentStep.Checkout:
|
||||
return lang('PaymentCheckout');
|
||||
case PaymentStep.ShippingInfo:
|
||||
return lang('PaymentShippingInfo');
|
||||
case PaymentStep.Shipping:
|
||||
return lang('PaymentShippingMethod');
|
||||
case PaymentStep.SavedPayments:
|
||||
return lang('PaymentCheckoutMethod');
|
||||
case PaymentStep.ConfirmPassword:
|
||||
return lang('Checkout.PasswordEntry.Title');
|
||||
case PaymentStep.PaymentInfo:
|
||||
return lang('PaymentCardInfo');
|
||||
case PaymentStep.Checkout:
|
||||
return lang('PaymentCheckout');
|
||||
case PaymentStep.ConfirmPayment:
|
||||
return lang('Checkout.WebConfirmation.Title');
|
||||
default:
|
||||
@ -322,14 +470,15 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
: lang('Next');
|
||||
|
||||
const isSubmitDisabled = isLoading
|
||||
|| Boolean(step === PaymentStep.Checkout && invoiceContent?.isRecurring && !isTosAccepted);
|
||||
|| Boolean(step === PaymentStep.Checkout && invoice?.isRecurring && !isTosAccepted);
|
||||
|
||||
if (isProviderError) {
|
||||
return (
|
||||
<Modal
|
||||
className="error"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
onCloseAnimationEnd={handleModalClose}
|
||||
>
|
||||
<p>
|
||||
Sorry, Telegram WebZ doesn't support payments with this provider yet. <br />
|
||||
@ -337,7 +486,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
</p>
|
||||
<Button
|
||||
isText
|
||||
onClick={onClose}
|
||||
onClick={closeModal}
|
||||
>
|
||||
{lang('OK')}
|
||||
</Button>
|
||||
@ -347,9 +496,9 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={buildClassName('PaymentModal', invoiceContent?.isRecurring && 'recurring')}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className={buildClassName('PaymentModal', invoice?.isRecurring && 'recurring')}
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
onCloseAnimationEnd={handleModalClose}
|
||||
>
|
||||
<div className="header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
@ -358,10 +507,10 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
color="translucent"
|
||||
round
|
||||
size="smaller"
|
||||
onClick={onClose}
|
||||
onClick={step === PaymentStep.Checkout ? closeModal : handleBackClick}
|
||||
ariaLabel="Close"
|
||||
>
|
||||
<i className="icon-close" />
|
||||
<i className={step === PaymentStep.Checkout ? 'icon-close' : 'icon-arrow-left'} />
|
||||
</Button>
|
||||
<h3>{modalHeader}</h3>
|
||||
</div>
|
||||
@ -401,29 +550,33 @@ export default memo(withGlobal<OwnProps>(
|
||||
savedInfo,
|
||||
canSaveCredentials,
|
||||
invoice,
|
||||
invoiceContent,
|
||||
invoiceContainer,
|
||||
nativeProvider,
|
||||
nativeParams,
|
||||
passwordMissing,
|
||||
error,
|
||||
confirmPaymentUrl,
|
||||
inputInvoice,
|
||||
requestId,
|
||||
stripeCredentials,
|
||||
smartGlocalCredentials,
|
||||
savedCredentials,
|
||||
temporaryPassword,
|
||||
} = global.payment;
|
||||
|
||||
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId) : undefined;
|
||||
const isProviderError = Boolean(invoice && (!nativeProvider || !SUPPORTED_PROVIDERS.has(nativeProvider)));
|
||||
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
|
||||
const {
|
||||
nameRequested,
|
||||
phoneRequested,
|
||||
emailRequested,
|
||||
shippingAddressRequested,
|
||||
flexible,
|
||||
phoneToProvider,
|
||||
emailToProvider,
|
||||
isNameRequested,
|
||||
isShippingAddressRequested,
|
||||
isPhoneRequested,
|
||||
isEmailRequested,
|
||||
shouldSendPhoneToProvider,
|
||||
shouldSendEmailToProvider,
|
||||
currency,
|
||||
prices,
|
||||
} = (invoice || {});
|
||||
} = (invoiceContainer || {});
|
||||
|
||||
return {
|
||||
step,
|
||||
@ -433,23 +586,28 @@ export default memo(withGlobal<OwnProps>(
|
||||
canSaveCredentials,
|
||||
nativeProvider,
|
||||
passwordMissing,
|
||||
nameRequested,
|
||||
shippingAddressRequested,
|
||||
phoneRequested,
|
||||
emailRequested,
|
||||
flexible,
|
||||
phoneToProvider,
|
||||
emailToProvider,
|
||||
isNameRequested,
|
||||
isShippingAddressRequested,
|
||||
isPhoneRequested,
|
||||
isEmailRequested,
|
||||
shouldSendPhoneToProvider,
|
||||
shouldSendEmailToProvider,
|
||||
currency,
|
||||
prices,
|
||||
isProviderError,
|
||||
invoiceContent,
|
||||
invoice,
|
||||
needCardholderName,
|
||||
needCountry,
|
||||
needZip,
|
||||
error,
|
||||
confirmPaymentUrl,
|
||||
countryList: global.countryList.general,
|
||||
requestId,
|
||||
hasShippingOptions: Boolean(shippingOptions?.length),
|
||||
smartGlocalToken: smartGlocalCredentials?.token,
|
||||
stripeId: stripeCredentials?.id,
|
||||
savedCredentials,
|
||||
passwordValidUntil: temporaryPassword?.validUntil,
|
||||
};
|
||||
},
|
||||
)(PaymentModal));
|
||||
@ -463,11 +621,16 @@ function getShippingPrices(shippingOptions: ShippingOption[], shippingOption: st
|
||||
return option?.prices;
|
||||
}
|
||||
|
||||
function getTotalPrice(prices: Price[] = [], shippingOptions: ShippingOption[] | undefined, shippingOption: string) {
|
||||
function getTotalPrice(
|
||||
prices: Price[] = [],
|
||||
shippingOptions: ShippingOption[] | undefined,
|
||||
shippingOption: string,
|
||||
tipAmount: number,
|
||||
) {
|
||||
const shippingPrices = shippingOptions
|
||||
? getShippingPrices(shippingOptions, shippingOption)
|
||||
: [];
|
||||
let total = 0;
|
||||
let total = tipAmount;
|
||||
const totalPrices = prices.concat(shippingPrices || []);
|
||||
total = totalPrices.reduce((acc, cur) => {
|
||||
return acc + cur.amount;
|
||||
@ -477,7 +640,7 @@ function getTotalPrice(prices: Price[] = [], shippingOptions: ShippingOption[] |
|
||||
|
||||
function getCheckoutInfo(state: FormState, shippingOptions: ShippingOption[] | undefined, paymentProvider: string) {
|
||||
const cardTypeText = detectCardTypeText(state.cardNumber);
|
||||
const paymentMethod = `${cardTypeText} *${state.cardNumber.slice(-4)}`;
|
||||
const paymentMethod = cardTypeText && state.cardNumber ? `${cardTypeText} *${state.cardNumber.slice(-4)}` : undefined;
|
||||
const shippingAddress = state.streetLine1
|
||||
? `${state.streetLine1}, ${state.city}, ${state.countryIso2}`
|
||||
: undefined;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useEffect } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import type { Price } from '../../types';
|
||||
import type { ApiShippingAddress, ApiWebDocument } from '../../api/types';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
|
||||
import Checkout from './Checkout';
|
||||
import Modal from '../ui/Modal';
|
||||
@ -21,6 +23,7 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
prices?: Price[];
|
||||
shippingPrices: any;
|
||||
tipAmount?: number;
|
||||
totalAmount?: number;
|
||||
currency?: string;
|
||||
info?: {
|
||||
@ -40,6 +43,7 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
|
||||
onClose,
|
||||
prices,
|
||||
shippingPrices,
|
||||
tipAmount,
|
||||
totalAmount,
|
||||
currency,
|
||||
info,
|
||||
@ -50,15 +54,35 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
|
||||
shippingMethod,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
openModal();
|
||||
}
|
||||
}, [isOpen, openModal]);
|
||||
|
||||
const checkoutInfo = useMemo(() => {
|
||||
return getCheckoutInfo(credentialsTitle, info, shippingMethod);
|
||||
}, [info, shippingMethod, credentialsTitle]);
|
||||
|
||||
const invoice = useMemo(() => {
|
||||
return {
|
||||
photo,
|
||||
text: text!,
|
||||
title: title!,
|
||||
amount: totalAmount!,
|
||||
currency: currency!,
|
||||
};
|
||||
}, [currency, photo, text, title, totalAmount]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="PaymentModal PaymentModal-receipt"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
onCloseAnimationEnd={onClose}
|
||||
>
|
||||
<div>
|
||||
<div className="header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
@ -67,7 +91,7 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
|
||||
color="translucent"
|
||||
round
|
||||
size="smaller"
|
||||
onClick={onClose}
|
||||
onClick={closeModal}
|
||||
ariaLabel="Close"
|
||||
>
|
||||
<i className="icon-close" />
|
||||
@ -79,11 +103,8 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
|
||||
prices={prices}
|
||||
shippingPrices={shippingPrices}
|
||||
totalPrice={totalAmount}
|
||||
invoiceContent={{
|
||||
photo,
|
||||
text,
|
||||
title,
|
||||
}}
|
||||
tipAmount={tipAmount}
|
||||
invoice={invoice}
|
||||
checkoutInfo={checkoutInfo}
|
||||
currency={currency!}
|
||||
/>
|
||||
@ -107,12 +128,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
photo,
|
||||
text,
|
||||
title,
|
||||
tipAmount,
|
||||
} = (receipt || {});
|
||||
|
||||
return {
|
||||
currency,
|
||||
prices,
|
||||
info,
|
||||
tipAmount,
|
||||
totalAmount,
|
||||
credentialsTitle,
|
||||
shippingPrices,
|
||||
|
||||
58
src/components/payment/SavedPaymentCredentials.tsx
Normal file
58
src/components/payment/SavedPaymentCredentials.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { memo, useCallback, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiPaymentCredentials } from '../../api/types';
|
||||
import type { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
|
||||
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import RadioGroup from '../ui/RadioGroup';
|
||||
|
||||
interface OwnProps {
|
||||
state: FormState;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
dispatch: FormEditDispatch;
|
||||
onNewCardClick: NoneToVoidFunction;
|
||||
}
|
||||
|
||||
const SavedPaymentCredentials: FC<OwnProps> = ({
|
||||
state,
|
||||
savedCredentials,
|
||||
dispatch,
|
||||
onNewCardClick,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return savedCredentials?.length
|
||||
? savedCredentials.map(({ id, title }) => ({ label: title, value: id }))
|
||||
: MEMO_EMPTY_ARRAY;
|
||||
}, [savedCredentials]);
|
||||
|
||||
const onChange = useCallback((value) => {
|
||||
dispatch({ type: 'changeSavedCredentialId', payload: value });
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="PaymentInfo">
|
||||
<form>
|
||||
<h5>{lang('PaymentCardTitle')}</h5>
|
||||
|
||||
<RadioGroup
|
||||
name="saved-credentials"
|
||||
options={options}
|
||||
selected={state.savedCredentialId}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<Button isText onClick={onNewCardClick}>
|
||||
{lang('PaymentCheckoutMethodNewCard')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SavedPaymentCredentials);
|
||||
@ -29,7 +29,7 @@ const Shipping: FC<OwnProps> = ({
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shippingOptions || state.shipping) {
|
||||
if (!shippingOptions || !shippingOptions.length || state.shipping) {
|
||||
return;
|
||||
}
|
||||
dispatch({ type: 'changeShipping', payload: shippingOptions[0].id });
|
||||
|
||||
@ -4,8 +4,8 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiCountry } from '../../api/types';
|
||||
|
||||
import type { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
|
||||
|
||||
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
|
||||
import type { ApiChat, ApiInvoice, ApiRequestInputInvoice } from '../../../api/types';
|
||||
import { PaymentStep } from '../../../types';
|
||||
import type { ApiChat, ApiRequestInputInvoice } from '../../../api/types';
|
||||
|
||||
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
|
||||
import {
|
||||
selectPaymentRequestId,
|
||||
selectProviderPublishableKey,
|
||||
@ -14,11 +16,8 @@ import {
|
||||
selectSmartGlocalCredentials,
|
||||
selectPaymentInputInvoice,
|
||||
} from '../../selectors';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { getStripeError } from '../../helpers';
|
||||
import { buildQueryString } from '../../../util/requestQuery';
|
||||
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
|
||||
|
||||
import {
|
||||
updateShippingOptions,
|
||||
setPaymentStep,
|
||||
@ -28,19 +27,25 @@ import {
|
||||
setReceipt,
|
||||
clearPayment,
|
||||
closeInvoice,
|
||||
setSmartGlocalCardInfo, addUsers, setInvoiceInfo,
|
||||
setSmartGlocalCardInfo, addUsers, setInvoiceInfo, updatePayment,
|
||||
} from '../../reducers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
|
||||
addActionHandler('validateRequestedInfo', (global, actions, payload) => {
|
||||
const { requestInfo, saveInfo } = payload;
|
||||
const inputInvoice = selectPaymentInputInvoice(global);
|
||||
if (!inputInvoice) return;
|
||||
if (!inputInvoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { requestInfo, saveInfo } = payload;
|
||||
if ('slug' in inputInvoice) {
|
||||
void validateRequestedInfo(inputInvoice, requestInfo, saveInfo);
|
||||
} else {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) return;
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
void validateRequestedInfo({
|
||||
chat,
|
||||
messageId: inputInvoice.messageId,
|
||||
@ -48,42 +53,25 @@ addActionHandler('validateRequestedInfo', (global, actions, payload) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function validateRequestedInfo(inputInvoice: ApiRequestInputInvoice, requestInfo: any, shouldSave?: true) {
|
||||
const result = await callApi('validateRequestedInfo', {
|
||||
inputInvoice, requestInfo, shouldSave,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, shippingOptions } = result;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let global = setRequestInfoId(getGlobal(), id);
|
||||
if (shippingOptions) {
|
||||
global = updateShippingOptions(global, shippingOptions);
|
||||
global = setPaymentStep(global, PaymentStep.Shipping);
|
||||
} else {
|
||||
global = setPaymentStep(global, PaymentStep.PaymentInfo);
|
||||
}
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
addActionHandler('openInvoice', async (global, actions, payload) => {
|
||||
let invoice;
|
||||
let invoice: ApiInvoice | undefined;
|
||||
if ('slug' in payload) {
|
||||
invoice = await getPaymentForm({ slug: payload.slug });
|
||||
} else {
|
||||
const chat = selectChat(global, payload.chatId);
|
||||
if (!chat) return;
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice = await getPaymentForm({
|
||||
chat,
|
||||
messageId: payload.messageId,
|
||||
});
|
||||
}
|
||||
if (!invoice) return;
|
||||
|
||||
if (!invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = setInvoiceInfo(global, invoice);
|
||||
@ -98,22 +86,18 @@ addActionHandler('openInvoice', async (global, actions, payload) => {
|
||||
});
|
||||
});
|
||||
|
||||
async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
|
||||
async function getPaymentForm(inputInvoice: ApiRequestInputInvoice): Promise<ApiInvoice | undefined> {
|
||||
const result = await callApi('getPaymentForm', inputInvoice);
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { form, invoice } = result;
|
||||
|
||||
let global = setPaymentForm(getGlobal(), form);
|
||||
let step = PaymentStep.PaymentInfo;
|
||||
const {
|
||||
shippingAddressRequested, nameRequested, phoneRequested, emailRequested,
|
||||
} = global.payment.invoice || {};
|
||||
if (shippingAddressRequested || nameRequested || phoneRequested || emailRequested) {
|
||||
step = PaymentStep.ShippingInfo;
|
||||
}
|
||||
global = setPaymentStep(global, step);
|
||||
global = setPaymentStep(global, PaymentStep.Checkout);
|
||||
setGlobal(global);
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
@ -179,17 +163,19 @@ addActionHandler('sendCredentialsInfo', (global, actions, payload) => {
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('sendPaymentForm', (global, actions, payload) => {
|
||||
const { shippingOptionId, saveCredentials } = payload;
|
||||
addActionHandler('sendPaymentForm', async (global, actions, payload) => {
|
||||
const {
|
||||
shippingOptionId, saveCredentials, savedCredentialId, tipAmount,
|
||||
} = payload;
|
||||
const inputInvoice = selectPaymentInputInvoice(global);
|
||||
const formId = selectPaymentFormId(global);
|
||||
const requestInfoId = selectPaymentRequestId(global);
|
||||
const { nativeProvider } = global.payment;
|
||||
const { nativeProvider, temporaryPassword } = global.payment;
|
||||
const publishableKey = nativeProvider === 'stripe'
|
||||
? selectProviderPublishableKey(global) : selectProviderPublicToken(global);
|
||||
|
||||
if (!inputInvoice || !publishableKey || !formId || !nativeProvider) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let requestInputInvoice;
|
||||
@ -200,7 +186,7 @@ addActionHandler('sendPaymentForm', (global, actions, payload) => {
|
||||
} else {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
requestInputInvoice = {
|
||||
@ -209,18 +195,32 @@ addActionHandler('sendPaymentForm', (global, actions, payload) => {
|
||||
};
|
||||
}
|
||||
|
||||
void sendPaymentForm(requestInputInvoice, formId, {
|
||||
setGlobal(updatePayment(global, { status: 'pending' }));
|
||||
|
||||
const credentials = {
|
||||
save: saveCredentials,
|
||||
data: nativeProvider === 'stripe' ? selectStripeCredentials(global) : selectSmartGlocalCredentials(global),
|
||||
}, requestInfoId, shippingOptionId);
|
||||
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
status: 'pending',
|
||||
},
|
||||
};
|
||||
const result = await callApi('sendPaymentForm', {
|
||||
inputInvoice: requestInputInvoice,
|
||||
formId,
|
||||
credentials,
|
||||
requestedInfoId: requestInfoId,
|
||||
shippingOptionId,
|
||||
savedCredentialId,
|
||||
temporaryPassword: temporaryPassword?.value,
|
||||
tipAmount,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = clearPayment(global);
|
||||
global = updatePayment(global, { status: 'paid' });
|
||||
global = closeInvoice(global);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
async function sendStripeCredentials(
|
||||
@ -288,10 +288,10 @@ async function sendSmartGlocalCredentials(
|
||||
) {
|
||||
const params = {
|
||||
card: {
|
||||
number: data.cardNumber.replace(/[^\d]+/g, ''),
|
||||
number: data.cardNumber.replace(/\D+/g, ''),
|
||||
expiration_month: data.expiryMonth,
|
||||
expiration_year: data.expiryYear,
|
||||
security_code: data.cvv.replace(/[^\d]+/g, ''),
|
||||
security_code: data.cvv.replace(/\D+/g, ''),
|
||||
},
|
||||
};
|
||||
const url = DEBUG_PAYMENT_SMART_GLOCAL
|
||||
@ -334,32 +334,8 @@ async function sendSmartGlocalCredentials(
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
async function sendPaymentForm(
|
||||
inputInvoice: ApiRequestInputInvoice,
|
||||
formId: string,
|
||||
credentials: any,
|
||||
requestedInfoId?: string,
|
||||
shippingOptionId?: string,
|
||||
) {
|
||||
const result = await callApi('sendPaymentForm', {
|
||||
inputInvoice, formId, credentials, requestedInfoId, shippingOptionId,
|
||||
});
|
||||
|
||||
if (result === true) {
|
||||
let global = clearPayment(getGlobal());
|
||||
global = {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
status: 'paid',
|
||||
},
|
||||
};
|
||||
setGlobal(closeInvoice(global));
|
||||
}
|
||||
}
|
||||
|
||||
addActionHandler('setPaymentStep', (global, actions, payload = {}) => {
|
||||
return setPaymentStep(global, payload.step || PaymentStep.ShippingInfo);
|
||||
return setPaymentStep(global, payload.step ?? PaymentStep.Checkout);
|
||||
});
|
||||
|
||||
addActionHandler('closePremiumModal', (global, actions, payload) => {
|
||||
@ -431,3 +407,39 @@ addActionHandler('closeGiftPremiumModal', (global) => {
|
||||
giftPremiumModal: { isOpen: false },
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('validatePaymentPassword', async (global, actions, { password }) => {
|
||||
const result = await callApi('fetchTemporaryPaymentPassword', password);
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
if (!result) {
|
||||
global = updatePayment(global, { error: { message: 'Unknown Error', field: 'password' } });
|
||||
} else if ('error' in result) {
|
||||
global = updatePayment(global, { error: { message: result.error, field: 'password' } });
|
||||
} else {
|
||||
global = updatePayment(global, { temporaryPassword: result, step: PaymentStep.Checkout });
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
async function validateRequestedInfo(inputInvoice: ApiRequestInputInvoice, requestInfo: any, shouldSave?: true) {
|
||||
const result = await callApi('validateRequestedInfo', {
|
||||
inputInvoice, requestInfo, shouldSave,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, shippingOptions } = result;
|
||||
|
||||
let global = setRequestInfoId(getGlobal(), id);
|
||||
if (shippingOptions) {
|
||||
global = updateShippingOptions(global, shippingOptions);
|
||||
global = setPaymentStep(global, PaymentStep.Shipping);
|
||||
} else {
|
||||
global = setPaymentStep(global, PaymentStep.Checkout);
|
||||
}
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
@ -2,11 +2,30 @@ import { addActionHandler } from '../../index';
|
||||
|
||||
import { IS_PRODUCTION_HOST } from '../../../util/environment';
|
||||
import { clearPayment } from '../../reducers';
|
||||
import * as langProvider from '../../../util/langProvider';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { selectChatMessage } from '../../selectors';
|
||||
|
||||
addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
switch (update['@type']) {
|
||||
case 'updatePaymentStateCompleted': {
|
||||
const { inputInvoice } = global.payment;
|
||||
|
||||
if (inputInvoice && 'chatId' in inputInvoice && 'messageId' in inputInvoice) {
|
||||
const message = selectChatMessage(global, inputInvoice.chatId, inputInvoice.messageId);
|
||||
|
||||
if (message && message.content.invoice) {
|
||||
const { amount, currency, title } = message.content.invoice;
|
||||
|
||||
actions.showNotification({
|
||||
message: langProvider.getTranslation('PaymentInfoHint', [
|
||||
formatCurrency(amount, currency, langProvider.getTranslation.code),
|
||||
title,
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// On the production host, the payment frame receives a message with the payment event,
|
||||
// after which the payment form closes. In other cases, the payment form must be closed manually.
|
||||
if (!IS_PRODUCTION_HOST) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
|
||||
import type { ApiError } from '../../../api/types';
|
||||
import type { ApiError, ApiNotification } from '../../../api/types';
|
||||
|
||||
import { APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT } from '../../../config';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment';
|
||||
@ -244,7 +244,7 @@ addActionHandler('showNotification', (global, actions, payload) => {
|
||||
newNotifications.splice(existingNotificationIndex, 1);
|
||||
}
|
||||
|
||||
newNotifications.push(notification);
|
||||
newNotifications.push(notification as ApiNotification);
|
||||
|
||||
return {
|
||||
...global,
|
||||
|
||||
@ -4,37 +4,29 @@ import type {
|
||||
ApiInvoice, ApiMessage, ApiPaymentForm, ApiReceipt,
|
||||
} from '../../api/types';
|
||||
|
||||
export function updatePayment(global: GlobalState, update: Partial<GlobalState['payment']>): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
...update,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateShippingOptions(
|
||||
global: GlobalState,
|
||||
shippingOptions: ShippingOption[],
|
||||
): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
shippingOptions,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { shippingOptions });
|
||||
}
|
||||
|
||||
export function setRequestInfoId(global: GlobalState, id: string): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
requestId: id,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { requestId: id });
|
||||
}
|
||||
|
||||
export function setPaymentStep(global: GlobalState, step: PaymentStep): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
step,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { step });
|
||||
}
|
||||
|
||||
export function setInvoiceInfo(global: GlobalState, invoice: ApiInvoice): GlobalState {
|
||||
@ -47,71 +39,43 @@ export function setInvoiceInfo(global: GlobalState, invoice: ApiInvoice): Global
|
||||
photo,
|
||||
isRecurring,
|
||||
recurringTermsUrl,
|
||||
maxTipAmount,
|
||||
suggestedTipAmounts,
|
||||
} = invoice;
|
||||
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
invoiceContent: {
|
||||
title,
|
||||
text,
|
||||
photo,
|
||||
amount,
|
||||
currency,
|
||||
isTest,
|
||||
isRecurring,
|
||||
recurringTermsUrl,
|
||||
},
|
||||
return updatePayment(global, {
|
||||
invoice: {
|
||||
title,
|
||||
text,
|
||||
photo,
|
||||
amount,
|
||||
currency,
|
||||
isTest,
|
||||
isRecurring,
|
||||
recurringTermsUrl,
|
||||
maxTipAmount,
|
||||
suggestedTipAmounts,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function setStripeCardInfo(global: GlobalState, cardInfo: { type: string; id: string }): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
stripeCredentials: {
|
||||
...cardInfo,
|
||||
},
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { stripeCredentials: { ...cardInfo } });
|
||||
}
|
||||
|
||||
export function setSmartGlocalCardInfo(
|
||||
global: GlobalState,
|
||||
cardInfo: { type: string; token: string },
|
||||
): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
smartGlocalCredentials: {
|
||||
...cardInfo,
|
||||
},
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { smartGlocalCredentials: { ...cardInfo } });
|
||||
}
|
||||
|
||||
export function setPaymentForm(global: GlobalState, form: ApiPaymentForm): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
...form,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { ...form });
|
||||
}
|
||||
|
||||
export function setConfirmPaymentUrl(global: GlobalState, url?: string): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
confirmPaymentUrl: url,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { confirmPaymentUrl: url });
|
||||
}
|
||||
|
||||
export function setReceipt(
|
||||
@ -120,13 +84,7 @@ export function setReceipt(
|
||||
message?: ApiMessage,
|
||||
): GlobalState {
|
||||
if (!receipt || !message) {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
receipt: undefined,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { receipt: undefined });
|
||||
}
|
||||
|
||||
const { invoice: messageInvoice } = message.content;
|
||||
@ -134,18 +92,14 @@ export function setReceipt(
|
||||
photo, text, title,
|
||||
} = (messageInvoice || {});
|
||||
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
receipt: {
|
||||
...receipt,
|
||||
photo,
|
||||
text,
|
||||
title,
|
||||
},
|
||||
return updatePayment(global, {
|
||||
receipt: {
|
||||
...receipt,
|
||||
photo,
|
||||
text,
|
||||
title,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function clearPayment(global: GlobalState): GlobalState {
|
||||
@ -156,11 +110,5 @@ export function clearPayment(global: GlobalState): GlobalState {
|
||||
}
|
||||
|
||||
export function closeInvoice(global: GlobalState): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
...global.payment,
|
||||
isPaymentModalOpen: false,
|
||||
},
|
||||
};
|
||||
return updatePayment(global, { isPaymentModalOpen: undefined });
|
||||
}
|
||||
|
||||
@ -44,6 +44,8 @@ import type {
|
||||
ApiInvoice,
|
||||
ApiStickerSetInfo,
|
||||
ApiChatType,
|
||||
ApiReceipt,
|
||||
ApiPaymentCredentials,
|
||||
} from '../api/types';
|
||||
import type {
|
||||
FocusDirection,
|
||||
@ -56,8 +58,7 @@ import type {
|
||||
ManagementProgress,
|
||||
PaymentStep,
|
||||
ShippingOption,
|
||||
Invoice,
|
||||
Receipt,
|
||||
ApiInvoiceContainer,
|
||||
ApiPrivacyKey,
|
||||
ApiPrivacySettings,
|
||||
ThemeKey,
|
||||
@ -481,8 +482,8 @@ export type GlobalState = {
|
||||
requestId?: string;
|
||||
savedInfo?: ApiPaymentSavedInfo;
|
||||
canSaveCredentials?: boolean;
|
||||
invoice?: Invoice;
|
||||
invoiceContent?: Omit<ApiInvoice, 'receiptMsgId'>;
|
||||
invoice?: ApiInvoice;
|
||||
invoiceContainer?: Omit<ApiInvoiceContainer, 'receiptMsgId'>;
|
||||
nativeProvider?: string;
|
||||
providerId?: string;
|
||||
nativeParams?: ApiPaymentFormNativeParams;
|
||||
@ -495,18 +496,19 @@ export type GlobalState = {
|
||||
token: string;
|
||||
};
|
||||
passwordMissing?: boolean;
|
||||
savedCredentials?: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
receipt?: Receipt;
|
||||
savedCredentials?: ApiPaymentCredentials[];
|
||||
receipt?: ApiReceipt;
|
||||
error?: {
|
||||
field?: string;
|
||||
message?: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
};
|
||||
isPaymentModalOpen?: boolean;
|
||||
confirmPaymentUrl?: string;
|
||||
temporaryPassword?: {
|
||||
value: string;
|
||||
validUntil: number;
|
||||
};
|
||||
};
|
||||
|
||||
chatCreation?: {
|
||||
@ -1107,6 +1109,15 @@ export interface ActionPayloads {
|
||||
url?: string;
|
||||
};
|
||||
closeUrlAuthModal: never;
|
||||
showNotification: {
|
||||
localId?: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
className?: string;
|
||||
actionText?: string;
|
||||
action?: NoneToVoidFunction;
|
||||
};
|
||||
dismissNotification: { localId: string };
|
||||
|
||||
// Calls
|
||||
requestCall: {
|
||||
@ -1180,12 +1191,17 @@ export interface ActionPayloads {
|
||||
|
||||
// Invoice
|
||||
openInvoice: ApiInputInvoice;
|
||||
|
||||
// Payment
|
||||
validatePaymentPassword: {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type NonTypedActionNames = (
|
||||
// system
|
||||
'init' | 'reset' | 'disconnect' | 'initApi' | 'sync' | 'saveSession' |
|
||||
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
||||
'showDialog' | 'dismissDialog' |
|
||||
// ui
|
||||
'toggleChatInfo' | 'setIsUiReady' | 'toggleLeftColumn' |
|
||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||
|
||||
@ -21,13 +21,16 @@ export type FormState = {
|
||||
saveInfo: boolean;
|
||||
saveCredentials: boolean;
|
||||
formErrors: Record<string, string>;
|
||||
tipAmount: number;
|
||||
savedCredentialId: string;
|
||||
};
|
||||
|
||||
export type FormActions = (
|
||||
'changeAddress1' | 'changeAddress2' | 'changeCity' | 'changeState' | 'changeCountry' |
|
||||
'changePostCode' | 'changeFullName' | 'changeEmail' | 'changePhone' | 'changeShipping' | 'updateUserInfo' |
|
||||
'changeCardNumber' | 'changeCardholder' | 'changeExpiryDate' | 'changeCvvCode' | 'changeBillingCountry' |
|
||||
'changeBillingZip' | 'changeSaveInfo' | 'changeSaveCredentials' | 'setFormErrors' | 'resetState'
|
||||
'changeBillingZip' | 'changeSaveInfo' | 'changeSaveCredentials' | 'setFormErrors' | 'resetState' | 'setTipAmount' |
|
||||
'changeSavedCredentialId'
|
||||
);
|
||||
export type FormEditDispatch = Dispatch<FormActions>;
|
||||
|
||||
@ -51,6 +54,8 @@ const INITIAL_STATE: FormState = {
|
||||
saveInfo: true,
|
||||
saveCredentials: false,
|
||||
formErrors: {},
|
||||
tipAmount: 0,
|
||||
savedCredentialId: '',
|
||||
};
|
||||
|
||||
const reducer: StateReducer<FormState, FormActions> = (state, action) => {
|
||||
@ -214,6 +219,16 @@ const reducer: StateReducer<FormState, FormActions> = (state, action) => {
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
case 'setTipAmount':
|
||||
return {
|
||||
...state,
|
||||
tipAmount: action.payload,
|
||||
};
|
||||
case 'changeSavedCredentialId':
|
||||
return {
|
||||
...state,
|
||||
savedCredentialId: action.payload,
|
||||
};
|
||||
case 'resetState':
|
||||
return {
|
||||
...INITIAL_STATE,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import TelegramClient from './TelegramClient';
|
||||
import type TelegramClient from './TelegramClient';
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import { default as Api } from '../tl/api';
|
||||
import { generateRandomBytes } from '../Helpers';
|
||||
@ -15,6 +15,8 @@ export interface TwoFaParams {
|
||||
onEmailCodeError?: (err: Error) => void;
|
||||
}
|
||||
|
||||
export type TmpPasswordResult = Api.account.TmpPassword | { error: string } | undefined;
|
||||
|
||||
/**
|
||||
* Changes the 2FA settings of the logged in user.
|
||||
Note that this method may be *incredibly* slow depending on the
|
||||
@ -121,3 +123,27 @@ export async function updateTwoFaSettings(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTmpPassword(client: TelegramClient, currentPassword: string, ttl = 60) {
|
||||
const pwd = await client.invoke(new Api.account.GetPassword());
|
||||
|
||||
if (!pwd) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inputPassword = await computeCheck(pwd, currentPassword);
|
||||
try {
|
||||
const result = await client.invoke(new Api.account.GetTmpPassword({
|
||||
password: inputPassword,
|
||||
period: ttl,
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
if (err.message === 'PASSWORD_HASH_INVALID') {
|
||||
return { error: err.message };
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
4
src/lib/gramjs/client/TelegramClient.d.ts
vendored
4
src/lib/gramjs/client/TelegramClient.d.ts
vendored
@ -3,7 +3,7 @@ import type { Api } from '..';
|
||||
import type { BotAuthParams, UserAuthParams } from './auth';
|
||||
import type { uploadFile, UploadFileParams } from './uploadFile';
|
||||
import type { downloadFile, DownloadFileParams } from './downloadFile';
|
||||
import type { TwoFaParams, updateTwoFaSettings } from './2fa';
|
||||
import type { TwoFaParams, updateTwoFaSettings, TmpPasswordResult } from './2fa';
|
||||
|
||||
declare class TelegramClient {
|
||||
constructor(...args: any);
|
||||
@ -18,6 +18,8 @@ declare class TelegramClient {
|
||||
|
||||
async updateTwoFaSettings(Params: TwoFaParams): ReturnType<typeof updateTwoFaSettings>;
|
||||
|
||||
async getTmpPassword(currentPassword: string, ttl?: number): Promise<TmpPasswordResult>;
|
||||
|
||||
// Untyped methods.
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ const {
|
||||
} = require('./auth');
|
||||
const { downloadFile } = require('./downloadFile');
|
||||
const { uploadFile } = require('./uploadFile');
|
||||
const { updateTwoFaSettings } = require('./2fa');
|
||||
const { updateTwoFaSettings, getTmpPassword } = require('./2fa');
|
||||
|
||||
const DEFAULT_DC_ID = 2;
|
||||
const WEBDOCUMENT_DC_ID = 4;
|
||||
@ -927,6 +927,10 @@ class TelegramClient {
|
||||
return updateTwoFaSettings(this, params);
|
||||
}
|
||||
|
||||
getTmpPassword(currentPassword, ttl) {
|
||||
return getTmpPassword(this, currentPassword, ttl);
|
||||
}
|
||||
|
||||
// event region
|
||||
addEventHandler(callback, event) {
|
||||
this._eventBuilders.push([event, callback]);
|
||||
|
||||
@ -102,7 +102,8 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--color-primary-shade: #{color.mix($color-primary, $color-black, 92%)};
|
||||
--color-primary-shade-darker: #{color.mix($color-primary, $color-black, 84%)};
|
||||
--color-primary-shade-rgb: #{toRGB(color.mix($color-primary, $color-black, 92%))};
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.5);
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.25);
|
||||
--color-primary-opacity-hover: rgba(var(--color-primary), 0.15);
|
||||
--color-green: #{$color-green};
|
||||
--color-green-darker: #{color.mix($color-green, $color-black, 84%)};
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"--color-primary": ["#3390EC", "#8774E1"],
|
||||
"--color-primary-opacity": ["#50A2E980", "#8378DB80"],
|
||||
"--color-primary-opacity": ["#50A2E940", "#8378DB80"],
|
||||
"--color-primary-opacity-hover": ["#50A2E926", "#8378DBA0"],
|
||||
"--color-primary-shade": ["#4a95d6", "#7b71c6"],
|
||||
"--color-background": ["#FFFFFF", "#212121"],
|
||||
"--color-background-compact-menu": ["#FFFFFFBB", "#212121DD"],
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||
ApiChatInviteImporter,
|
||||
ApiExportedInvite,
|
||||
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet, ApiWebDocument,
|
||||
ApiLanguage, ApiMessage, ApiStickerSet,
|
||||
} from '../api/types';
|
||||
|
||||
export type TextPart = TeactNode;
|
||||
@ -129,34 +129,17 @@ export interface Price {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
export interface ApiInvoiceContainer {
|
||||
isTest?: boolean;
|
||||
isNameRequested?: boolean;
|
||||
isPhoneRequested?: boolean;
|
||||
isEmailRequested?: boolean;
|
||||
isShippingAddressRequested?: boolean;
|
||||
isFlexible?: boolean;
|
||||
shouldSendPhoneToProvider?: boolean;
|
||||
shouldSendEmailToProvider?: boolean;
|
||||
currency?: string;
|
||||
emailRequested?: boolean;
|
||||
emailToProvider?: boolean;
|
||||
flexible?: boolean;
|
||||
nameRequested?: boolean;
|
||||
phoneRequested?: boolean;
|
||||
phoneToProvider?: boolean;
|
||||
prices?: Price[];
|
||||
shippingAddressRequested?: boolean;
|
||||
test?: boolean;
|
||||
}
|
||||
|
||||
export interface Receipt {
|
||||
currency: string;
|
||||
prices: Price[];
|
||||
info?: {
|
||||
shippingAddress?: ApiShippingAddress;
|
||||
phone?: string;
|
||||
name?: string;
|
||||
};
|
||||
totalAmount: number;
|
||||
credentialsTitle: string;
|
||||
shippingPrices?: Price[];
|
||||
shippingMethod?: string;
|
||||
photo?: ApiWebDocument;
|
||||
text?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export enum SettingsScreens {
|
||||
@ -346,10 +329,12 @@ export enum ProfileState {
|
||||
}
|
||||
|
||||
export enum PaymentStep {
|
||||
Checkout,
|
||||
SavedPayments,
|
||||
ConfirmPassword,
|
||||
PaymentInfo,
|
||||
ShippingInfo,
|
||||
Shipping,
|
||||
PaymentInfo,
|
||||
Checkout,
|
||||
ConfirmPayment,
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
import type { LangCode } from '../types';
|
||||
|
||||
export function formatCurrency(totalPrice: number, currency: string, locale: LangCode = 'en') {
|
||||
export function formatCurrency(
|
||||
totalPrice: number,
|
||||
currency: string,
|
||||
locale: LangCode = 'en',
|
||||
shouldOmitFractions = false,
|
||||
) {
|
||||
const price = totalPrice / 10 ** getCurrencyExp(currency);
|
||||
|
||||
if (shouldOmitFractions && price % 1 === 0) {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(totalPrice / 10 ** getCurrencyExp(currency));
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
function getCurrencyExp(currency: string) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type React from '../lib/teact/teact';
|
||||
|
||||
const stopEvent = (e: React.UIEvent | Event) => {
|
||||
const stopEvent = (e: React.UIEvent | Event | React.FormEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user