Payments: Support more providers (#1641)

This commit is contained in:
Alexander Zinchuk 2022-02-25 22:52:29 +02:00
parent f61db93008
commit d6a9ba5683
30 changed files with 324 additions and 79 deletions

View File

@ -116,6 +116,7 @@ export function buildPaymentForm(form: GramJs.payments.PaymentForm) {
needCountry: nativeData.need_country,
needZip: nativeData.need_zip,
publishableKey: nativeData.publishable_key,
publicToken: nativeData?.public_token,
},
};
}

View File

@ -3,7 +3,13 @@ import { Api as GramJs } from '../../../lib/gramjs';
import { invokeRequest } from './client';
import { buildInputPeer, buildShippingInfo } from '../gramjsBuilders';
import { buildShippingOptions, buildPaymentForm, buildReceipt } from '../apiBuilders/payments';
import { ApiChat } from '../../types';
import { ApiChat, OnApiUpdate } from '../../types';
let onUpdate: OnApiUpdate;
export function init(_onUpdate: OnApiUpdate) {
onUpdate = _onUpdate;
}
export async function validateRequestedInfo({
chat,
@ -40,7 +46,7 @@ export async function validateRequestedInfo({
};
}
export function sendPaymentForm({
export async function sendPaymentForm({
chat,
messageId,
formId,
@ -55,7 +61,7 @@ export function sendPaymentForm({
requestedInfoId?: string;
shippingOptionId?: string;
}) {
return invokeRequest(new GramJs.payments.SendPaymentForm({
const result = await invokeRequest(new GramJs.payments.SendPaymentForm({
formId: BigInt(formId),
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: messageId,
@ -65,7 +71,18 @@ export function sendPaymentForm({
save: credentials.save,
data: new GramJs.DataJSON({ data: JSON.stringify(credentials.data) }),
}),
}), true);
}));
if (result instanceof GramJs.payments.PaymentVerificationNeeded) {
onUpdate({
'@type': 'updatePaymentVerificationNeeded',
url: result.url,
});
return undefined;
}
return Boolean(result);
}
export async function getPaymentForm({

View File

@ -18,6 +18,7 @@ import { init as initStickers } from './methods/symbols';
import { init as initManagement } from './methods/management';
import { init as initTwoFaSettings } from './methods/twoFaSettings';
import { init as initCalls } from './methods/calls';
import { init as initPayments } from './methods/payments';
import * as methods from './methods';
let onUpdate: OnApiUpdate;
@ -34,6 +35,7 @@ export async function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArg
initManagement(handleUpdate);
initTwoFaSettings(handleUpdate);
initCalls(handleUpdate);
initPayments(handleUpdate);
await initClient(handleUpdate, initialArgs);
}

View File

@ -188,7 +188,11 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
if (update.message instanceof GramJs.MessageService) {
const { action } = update.message;
if (action instanceof GramJs.MessageActionChatEditTitle) {
if (action instanceof GramJs.MessageActionPaymentSent) {
onUpdate({
'@type': 'updatePaymentStateCompleted',
});
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
onUpdate({
'@type': 'updateChat',
id: message.chatId,

View File

@ -32,12 +32,15 @@ export interface ApiPaymentForm {
currency?: string;
prices?: ApiLabeledPrice[];
};
nativeParams: {
needCardholderName: boolean;
needCountry: boolean;
needZip: boolean;
publishableKey: string;
};
nativeParams: ApiPaymentFormNativeParams;
}
export interface ApiPaymentFormNativeParams {
needCardholderName?: boolean;
needCountry?: boolean;
needZip?: boolean;
publishableKey?: string;
publicToken?: string;
}
export interface ApiLabeledPrice {

View File

@ -381,6 +381,15 @@ export type ApiUpdatePeerBlocked = {
isBlocked: boolean;
};
export type ApiUpdatePaymentVerificationNeeded = {
'@type': 'updatePaymentVerificationNeeded';
url: string;
};
export type ApiUpdatePaymentStateCompleted = {
'@type': 'updatePaymentStateCompleted';
};
export type ApiUpdatePrivacy = {
'@type': 'updatePrivacy';
key: 'phoneNumber' | 'lastSeen' | 'profilePhoto' | 'forwards' | 'chatInvite';
@ -467,7 +476,7 @@ export type ApiUpdate = (
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
ApiUpdatePendingJoinRequests
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted
);
export type OnApiUpdate = (update: ApiUpdate) => void;

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

View File

@ -22,7 +22,7 @@
@media (max-width: 600px) {
// Force rendering in the composite layer to fix the z-index rendering issue
transform: translate3d(0, 0, 10px);
transform: translate3d(0, 0, 0.625rem);
transform-style: preserve-3d;
}
}

View File

@ -7,7 +7,7 @@
width: 100%;
height: 3.375rem;
max-width: 274px;
transform: translate3d(-50%, 0, 10px);
transform: translate3d(-50%, 0, 0.625rem);
transition: opacity 0.3s ease-in;
pointer-events: none;

View File

@ -2,9 +2,10 @@ import React, {
FC, memo, useCallback, useState, useRef, useEffect,
} from '../../lib/teact/teact';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import { formatCardNumber } from '../middle/helpers/inputFormatters';
import { detectCardType, CardType } from '../common/helpers/detectCardType';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import useLang from '../../hooks/useLang';
import InputText from '../ui/InputText';
@ -22,6 +23,7 @@ export type OwnProps = {
};
const CardInput : FC<OwnProps> = ({ value, error, onChange }) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const cardNumberRef = useRef<HTMLInputElement>(null);
@ -51,7 +53,7 @@ const CardInput : FC<OwnProps> = ({ value, error, onChange }) => {
<span className="left-addon">{cardIcon}</span>
<InputText
ref={cardNumberRef}
label="Card number"
label={lang('PaymentCardNumber')}
onChange={handleChange}
value={value}
inputMode="numeric"

View File

@ -63,13 +63,21 @@
width: 1.5rem;
}
i.stripe-provider {
background: url("../../assets/stripe-logo.png") no-repeat center;
i.provider {
background: no-repeat center;
background-size: 2rem;
border-radius: 1rem;
height: 1.5rem;
}
i.provider.stripe {
background-image: url("../../assets/stripe-logo.png");
}
i.provider.smartglocal {
background-image: url("../../assets/smartglocal-logo.png");
}
.info {
.title {
font-size: 1rem;

View File

@ -5,6 +5,7 @@ import React, {
import { LangCode, Price } from '../../types';
import { formatCurrency } from '../../util/formatCurrency';
import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
import './Checkout.scss';
@ -70,12 +71,16 @@ const Checkout: FC<OwnProps> = ({
) }
</div>
<div className="invoice-info">
{paymentMethod && renderCheckoutItem('icon-card', paymentMethod, 'Payment method')}
{paymentProvider && renderCheckoutItem('stripe-provider', paymentProvider, 'Payment provider')}
{shippingAddress && renderCheckoutItem('icon-location', shippingAddress, 'Shipping address')}
{name && renderCheckoutItem('icon-user', name, 'Name')}
{phone && renderCheckoutItem('icon-phone', phone, 'Phone number')}
{shippingMethod && renderCheckoutItem('icon-truck', shippingMethod, 'Shipping method')}
{paymentMethod && renderCheckoutItem('icon-card', paymentMethod, lang('PaymentCheckoutMethod'))}
{paymentProvider && renderCheckoutItem(
buildClassName('provider', paymentProvider.toLowerCase()),
paymentProvider,
lang('PaymentCheckoutProvider'),
)}
{shippingAddress && renderCheckoutItem('icon-location', shippingAddress, lang('PaymentShippingAddress'))}
{name && renderCheckoutItem('icon-user', name, lang('PaymentCheckoutName'))}
{phone && renderCheckoutItem('icon-phone', phone, lang('PaymentCheckoutPhoneNumber'))}
{shippingMethod && renderCheckoutItem('icon-truck', shippingMethod, lang('PaymentCheckoutShippingMethod'))}
</div>
</div>
);

View File

@ -0,0 +1,13 @@
.ConfirmPayment {
display: flex;
height: 100%;
border-bottom-left-radius: var(--border-radius-default-small);
border-bottom-right-radius: var(--border-radius-default-small);
overflow: hidden;
&__content {
width: 100%;
height: 100%;
border: none;
}
}

View File

@ -0,0 +1,27 @@
import React, { FC, memo } from '../../lib/teact/teact';
import useLang from '../../hooks/useLang';
import './ConfirmPayment.scss';
export type OwnProps = {
url: string;
};
const ConfirmPayment: FC<OwnProps> = ({ url }) => {
const lang = useLang();
return (
<div className="ConfirmPayment">
<iframe
src={url}
title={lang('Checkout.WebConfirmation.Title')}
allow="payment"
sandbox="allow-forms allow-scripts allow-same-origin allow-top-navigation"
className="ConfirmPayment__content"
/>
</div>
);
};
export default memo(ConfirmPayment);

View File

@ -5,6 +5,7 @@ import React, {
import { formatCardExpiry } from '../middle/helpers/inputFormatters';
import InputText from '../ui/InputText';
import useLang from '../../hooks/useLang';
const MAX_FIELD_LENGTH = 5;
@ -15,6 +16,7 @@ export type OwnProps = {
};
const ExpiryInput : FC<OwnProps> = ({ value, error, onChange }) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const expiryInputRef = useRef<HTMLInputElement>(null);
@ -33,7 +35,7 @@ const ExpiryInput : FC<OwnProps> = ({ value, error, onChange }) => {
return (
<InputText
label="Expiry date"
label={lang('PaymentCardExpireDate')}
ref={expiryInputRef}
onChange={handleChange}
onKeyDown={handleKeyDown}

View File

@ -84,7 +84,7 @@ const PaymentInfo: FC<OwnProps> = ({
/>
{ needCardholderName && (
<InputText
label="Name on card"
label={lang('PaymentCardName')}
onChange={handleCardholderChange}
value={state.cardholder}
inputMode="text"
@ -98,7 +98,7 @@ const PaymentInfo: FC<OwnProps> = ({
error={formErrors.expiry}
/>
<InputText
label="CVV code"
label={lang('lng_payments_card_cvc')}
onChange={handleCvvChange}
value={state.cvv}
inputMode="numeric"
@ -111,8 +111,8 @@ const PaymentInfo: FC<OwnProps> = ({
) : undefined }
{ needCountry && (
<Select
label="Country"
placeholder="Country"
label={lang('PaymentShippingCountry')}
placeholder={lang('PaymentShippingCountry')}
onChange={handleCountryChange}
value={state.billingCountry}
hasArrow={Boolean(true)}
@ -134,7 +134,7 @@ const PaymentInfo: FC<OwnProps> = ({
) }
{ needZip && (
<InputText
label="Post Code"
label={lang('PaymentShippingZipPlaceholder')}
onChange={handleBillingPostCodeChange}
value={state.billingZip}
inputMode="text"

View File

@ -5,8 +5,8 @@
.header {
position: relative;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top-left-radius: var(--border-radius-default-small);
border-top-right-radius: var(--border-radius-default-small);
width: 100%;
padding: 0.25rem 1rem;
display: flex;
@ -49,8 +49,8 @@
.footer {
position: relative;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: var(--border-radius-default-small);
border-bottom-right-radius: var(--border-radius-default-small);
width: 100%;
padding: 0.75rem 1rem;
background: var(--color-background);

View File

@ -19,10 +19,13 @@ import Button from '../ui/Button';
import Modal from '../ui/Modal';
import Transition from '../ui/Transition';
import Spinner from '../ui/Spinner';
import ConfirmPayment from './ConfirmPayment';
import './PaymentModal.scss';
const DEFAULT_PROVIDER = 'stripe';
const DONATE_PROVIDER = 'smartglocal';
const SUPPORTED_PROVIDERS = new Set([DEFAULT_PROVIDER, DONATE_PROVIDER]);
export type OwnProps = {
isOpen: boolean;
@ -43,6 +46,7 @@ type StateProps = {
needCardholderName?: boolean;
needCountry?: boolean;
needZip?: boolean;
confirmPaymentUrl?: string;
};
type GlobalStateProps = Pick<GlobalState['payment'], (
@ -73,6 +77,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps> = ({
needCardholderName,
needCountry,
needZip,
confirmPaymentUrl,
error,
}) => {
const {
@ -86,6 +91,7 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps> = ({
const [paymentState, paymentDispatch] = usePaymentReducer();
const [isLoading, setIsLoading] = useState(false);
const lang = useLang();
const canRenderFooter = step !== PaymentStep.ConfirmPayment;
useEffect(() => {
if (step || error) {
@ -210,6 +216,12 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps> = ({
currency={currency}
/>
);
case PaymentStep.ConfirmPayment:
return (
<ConfirmPayment
url={confirmPaymentUrl!}
/>
);
default:
return undefined;
}
@ -266,6 +278,8 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps> = ({
return lang('PaymentCardInfo');
case PaymentStep.Checkout:
return lang('PaymentCheckout');
case PaymentStep.ConfirmPayment:
return lang('Checkout.WebConfirmation.Title');
default:
return '';
}
@ -331,16 +345,18 @@ const Invoice: FC<OwnProps & StateProps & GlobalStateProps> = ({
<Spinner color="gray" />
</div>
)}
<div className="footer">
<Button
type="submit"
onClick={handleButtonClick}
disabled={isLoading}
isLoading={isLoading}
>
{buttonText}
</Button>
</div>
{canRenderFooter && (
<div className="footer">
<Button
type="submit"
onClick={handleButtonClick}
disabled={isLoading}
isLoading={isLoading}
>
{buttonText}
</Button>
</div>
)}
{error && !error.field && renderError()}
</Modal>
);
@ -359,9 +375,10 @@ export default memo(withGlobal<OwnProps>(
nativeParams,
passwordMissing,
error,
confirmPaymentUrl,
} = global.payment;
const isProviderError = Boolean(invoice && (!nativeProvider || nativeProvider !== DEFAULT_PROVIDER));
const isProviderError = Boolean(invoice && (!nativeProvider || !SUPPORTED_PROVIDERS.has(nativeProvider)));
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
const {
nameRequested,
@ -397,6 +414,7 @@ export default memo(withGlobal<OwnProps>(
needCountry,
needZip,
error,
confirmPaymentUrl,
};
},
)(Invoice));

View File

@ -47,7 +47,7 @@ const Shipping: FC<OwnProps> = ({
return (
<div className="Shipping">
<form>
<p>Select shipping method</p>
<p>{lang('PaymentShippingMethod')}</p>
<RadioGroup
name="shipping-options"
options={options}

View File

@ -102,36 +102,36 @@ const ShippingInfo: FC<OwnProps> = ({
<h5>{lang('PaymentShippingAddress')}</h5>
<InputText
ref={inputRef}
label="Address1 (Street)"
label={lang('PaymentShippingAddress1Placeholder')}
onChange={handleAddress1Change}
value={state.streetLine1}
inputMode="text"
error={formErrors.streetLine1}
/>
<InputText
label="Address2 (Street)"
label={lang('PaymentShippingAddress2Placeholder')}
onChange={handleAddress2Change}
value={state.streetLine2}
inputMode="text"
error={formErrors.streetLine2}
/>
<InputText
label="City"
label={lang('PaymentShippingCityPlaceholder')}
onChange={handleCityChange}
value={state.city}
inputMode="text"
error={formErrors.city}
/>
<InputText
label="State"
label={lang('PaymentShippingStatePlaceholder')}
onChange={handleStateChange}
value={state.state}
inputMode="text"
error={formErrors.state}
/>
<Select
label="Country"
placeholder="Country"
label={lang('PaymentShippingCountry')}
placeholder={lang('PaymentShippingCountry')}
onChange={handleCountryChange}
value={state.countryIso2}
hasArrow={Boolean(true)}
@ -150,7 +150,7 @@ const ShippingInfo: FC<OwnProps> = ({
</Select>
<InputText
label="Post Code"
label={lang('PaymentShippingZipPlaceholder')}
onChange={handlePostCodeChange}
value={state.postCode}
inputMode="text"
@ -163,7 +163,7 @@ const ShippingInfo: FC<OwnProps> = ({
) : undefined }
{ needName && (
<InputText
label="Full name"
label={lang('PaymentShippingName')}
onChange={handleFullNameChange}
value={state.fullName}
inputMode="text"
@ -172,7 +172,7 @@ const ShippingInfo: FC<OwnProps> = ({
) }
{ needEmail && (
<InputText
label="Email"
label={lang('PaymentShippingEmailPlaceholder')}
onChange={handleEmailChange}
value={state.email}
inputMode="email"
@ -181,7 +181,7 @@ const ShippingInfo: FC<OwnProps> = ({
) }
{ needPhone && (
<InputText
label="Phone number"
label={lang('PaymentShippingPhoneNumber')}
onChange={handlePhoneChange}
value={state.phone}
inputMode="tel"

View File

@ -16,6 +16,8 @@ export const DEBUG_GRAMJS = false;
export const PAGE_TITLE = 'Telegram';
export const INACTIVE_MARKER = ' [Inactive]';
export const DEBUG_PAYMENT_SMART_GLOCAL = false;
export const SESSION_USER_KEY = 'user_auth';
export const LEGACY_SESSION_KEY = 'GramJs:sessionId';

View File

@ -26,6 +26,7 @@ import {
ApiAvailableReaction,
ApiAppConfig,
ApiSponsoredMessage,
ApiPaymentFormNativeParams,
} from '../api/types';
import {
FocusDirection,
@ -426,16 +427,15 @@ export type GlobalState = {
};
nativeProvider?: string;
providerId?: string;
nativeParams?: {
needCardholderName: boolean;
needCountry: boolean;
needZip: boolean;
publishableKey: string;
};
nativeParams?: ApiPaymentFormNativeParams;
stripeCredentials?: {
type: string;
id: string;
};
smartGlocalCredentials?: {
type: string;
token: string;
};
passwordMissing?: boolean;
savedCredentials?: {
id: string;
@ -448,6 +448,7 @@ export type GlobalState = {
description: string;
};
isPaymentModalOpen?: boolean;
confirmPaymentUrl?: string;
};
chatCreation?: {

View File

@ -34,3 +34,4 @@ import './apiUpdaters/misc';
import './apiUpdaters/settings';
import './apiUpdaters/twoFaSettings';
import './apiUpdaters/calls';
import './apiUpdaters/payments';

View File

@ -12,10 +12,13 @@ import {
selectPaymentChatId,
selectChat,
selectPaymentFormId,
selectProviderPublicToken,
selectSmartGlocalCredentials,
} from '../../selectors';
import { callApi } from '../../../api/gramjs';
import { getStripeError } from '../../helpers';
import { buildQueryString } from '../../../util/requestQuery';
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
import {
updateShippingOptions,
@ -27,6 +30,7 @@ import {
setReceipt,
clearPayment,
closeInvoice,
setSmartGlocalCardInfo,
} from '../../reducers';
addReducer('validateRequestedInfo', (global, actions, payload) => {
@ -132,13 +136,23 @@ addReducer('clearReceipt', (global) => {
});
addReducer('sendCredentialsInfo', (global, actions, payload) => {
const publishableKey = selectProviderPublishableKey(global);
if (!publishableKey) {
return;
}
const { nativeProvider } = global.payment;
const { credentials } = payload;
const { data } = credentials;
void sendStripeCredentials(data, publishableKey);
if (nativeProvider === 'stripe') {
const publishableKey = selectProviderPublishableKey(global);
if (!publishableKey) {
return;
}
void sendStripeCredentials(data, publishableKey);
} else if (nativeProvider === 'smartglocal') {
const publicToken = selectProviderPublicToken(global);
if (!publicToken) {
return;
}
void sendSmartGlocalCredentials(data, publicToken);
}
});
addReducer('sendPaymentForm', (global, actions, payload) => {
@ -148,15 +162,16 @@ addReducer('sendPaymentForm', (global, actions, payload) => {
const messageId = selectPaymentMessageId(global);
const formId = selectPaymentFormId(global);
const requestInfoId = selectPaymentRequestId(global);
const publishableKey = selectProviderPublishableKey(global);
const stripeCredentials = selectStripeCredentials(global);
if (!chat || !messageId || !publishableKey || !formId) {
const { nativeProvider } = global.payment;
const publishableKey = nativeProvider === 'stripe'
? selectProviderPublishableKey(global) : selectProviderPublicToken(global);
if (!chat || !messageId || !publishableKey || !formId || !nativeProvider) {
return;
}
void sendPaymentForm(chat, messageId, formId, {
void sendPaymentForm(chat, messageId, nativeProvider, formId, {
save: saveCredentials,
data: stripeCredentials,
data: nativeProvider === 'stripe' ? selectStripeCredentials(global) : selectSmartGlocalCredentials(global),
}, requestInfoId, shippingOptionId);
});
@ -212,9 +227,67 @@ async function sendStripeCredentials(
setGlobal(global);
}
async function sendSmartGlocalCredentials(
data: {
cardNumber: string;
cardholder?: string;
expiryMonth: string;
expiryYear: string;
cvv: string;
},
publicToken: string,
) {
const params = {
card: {
number: data.cardNumber.replace(/[^\d]+/g, ''),
expiration_month: data.expiryMonth,
expiration_year: data.expiryYear,
security_code: data.cvv.replace(/[^\d]+/g, ''),
},
};
const url = DEBUG_PAYMENT_SMART_GLOCAL
? 'https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card'
: 'https://tgb.smart-glocal.com/cds/v1/tokenize/card';
const response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-PUBLIC-TOKEN': publicToken,
},
body: JSON.stringify(params),
});
const result = await response.json();
if (result.status !== 'ok') {
// TODO после получения документации сделать аналог getStripeError(result.error);
const error = { description: 'payment error' };
const global = getGlobal();
setGlobal({
...global,
payment: {
...global.payment,
error: {
...error,
},
},
});
return;
}
let global = setSmartGlocalCardInfo(getGlobal(), {
type: 'card',
token: result.data.token,
});
global = setPaymentStep(global, PaymentStep.Checkout);
setGlobal(global);
}
async function sendPaymentForm(
chat: ApiChat,
messageId: number,
nativeProvider: string,
formId: string,
credentials: any,
requestedInfoId?: string,
@ -223,7 +296,8 @@ async function sendPaymentForm(
const result = await callApi('sendPaymentForm', {
chat, messageId, formId, credentials, requestedInfoId, shippingOptionId,
});
if (result) {
if (result === true) {
const global = clearPayment(getGlobal());
setGlobal(closeInvoice(global));
}

View File

@ -1,11 +1,11 @@
import {
addReducer, getGlobal, setGlobal,
} from '../../../lib/teact/teactn';
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import { ApiUpdate } from '../../../api/types';
import { ApiPrivacyKey } from '../../../types';
import { ApiPrivacyKey, PaymentStep } from '../../../types';
import { addBlockedContact, removeBlockedContact } from '../../reducers';
import {
addBlockedContact, removeBlockedContact, setConfirmPaymentUrl, setPaymentStep,
} from '../../reducers';
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
switch (update['@type']) {
@ -32,6 +32,12 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
case 'updatePrivacy':
global.settings.privacy[update.key as ApiPrivacyKey] = update.rules;
break;
case 'updatePaymentVerificationNeeded':
global = setConfirmPaymentUrl(getGlobal(), update.url);
global = setPaymentStep(global, PaymentStep.ConfirmPayment);
setGlobal(global);
break;
}
return undefined;

View File

@ -0,0 +1,15 @@
import { addReducer } from '../../../lib/teact/teactn';
import { ApiUpdate } from '../../../api/types';
import { clearPayment } from '../../reducers';
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
switch (update['@type']) {
case 'updatePaymentStateCompleted': {
return clearPayment(global);
}
}
return undefined;
});

View File

@ -75,6 +75,21 @@ export function setStripeCardInfo(global: GlobalState, cardInfo: { type: string;
};
}
export function setSmartGlocalCardInfo(
global: GlobalState,
cardInfo: { type: string; token: string },
): GlobalState {
return {
...global,
payment: {
...global.payment,
smartGlocalCredentials: {
...cardInfo,
},
},
};
}
export function setPaymentForm(global: GlobalState, form: ApiPaymentForm): GlobalState {
return {
...global,
@ -85,6 +100,16 @@ export function setPaymentForm(global: GlobalState, form: ApiPaymentForm): Globa
};
}
export function setConfirmPaymentUrl(global: GlobalState, url?: string): GlobalState {
return {
...global,
payment: {
...global.payment,
confirmPaymentUrl: url,
},
};
}
export function setReceipt(
global: GlobalState,
receipt?: ApiReceipt,

View File

@ -17,9 +17,17 @@ export function selectPaymentRequestId(global: GlobalState) {
}
export function selectProviderPublishableKey(global: GlobalState) {
return global.payment.nativeParams ? global.payment.nativeParams.publishableKey : undefined;
return global.payment.nativeParams?.publishableKey;
}
export function selectProviderPublicToken(global: GlobalState) {
return global.payment.nativeParams?.publicToken;
}
export function selectStripeCredentials(global: GlobalState) {
return global.payment.stripeCredentials;
}
export function selectSmartGlocalCredentials(global: GlobalState) {
return global.payment.smartGlocalCredentials;
}

View File

@ -318,6 +318,7 @@ export enum PaymentStep {
Shipping,
PaymentInfo,
Checkout,
ConfirmPayment,
}
export const UPLOADING_WALLPAPER_SLUG = 'UPLOADING_WALLPAPER_SLUG';

View File

@ -69,6 +69,7 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
ADMIN_RANK_INVALID: 'The specified admin rank is invalid',
FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet',
INPUT_USER_DEACTIVATED: 'The specified user was deleted',
BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired',
};
export const SHIPPING_ERRORS: Record<string, ApiFieldError> = {