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