Payments: Support more providers (#1641)
This commit is contained in:
parent
f61db93008
commit
d6a9ba5683
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
src/assets/smartglocal-logo.png
Normal file
BIN
src/assets/smartglocal-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 402 B |
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
13
src/components/payment/ConfirmPayment.scss
Normal file
13
src/components/payment/ConfirmPayment.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
27
src/components/payment/ConfirmPayment.tsx
Normal file
27
src/components/payment/ConfirmPayment.tsx
Normal 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);
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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?: {
|
||||
|
||||
@ -34,3 +34,4 @@ import './apiUpdaters/misc';
|
||||
import './apiUpdaters/settings';
|
||||
import './apiUpdaters/twoFaSettings';
|
||||
import './apiUpdaters/calls';
|
||||
import './apiUpdaters/payments';
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
15
src/modules/actions/apiUpdaters/payments.ts
Normal file
15
src/modules/actions/apiUpdaters/payments.ts
Normal 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;
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -318,6 +318,7 @@ export enum PaymentStep {
|
||||
Shipping,
|
||||
PaymentInfo,
|
||||
Checkout,
|
||||
ConfirmPayment,
|
||||
}
|
||||
|
||||
export const UPLOADING_WALLPAPER_SLUG = 'UPLOADING_WALLPAPER_SLUG';
|
||||
|
||||
@ -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> = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user