Payments fixes

This commit is contained in:
Eduard Kuzmenko 2022-07-18 20:32:00 +02:00
parent ff4652530e
commit 2c9d5c30fe
10 changed files with 176 additions and 106 deletions

View File

@ -1588,11 +1588,17 @@ export default class ChatBubbles {
if(typeof(peerIdStr) === 'string' || savedFrom) { if(typeof(peerIdStr) === 'string' || savedFrom) {
if(savedFrom) { if(savedFrom) {
const [peerId, mid] = savedFrom.split('_'); const [peerId, mid] = savedFrom.split('_');
if(target.classList.contains('is-receipt-link')) {
this.chat.appImManager.setInnerPeer({ const message = await this.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), +mid);
peerId: peerId.toPeerId(), if(message) {
lastMsgId: +mid new PopupPayment(message as Message.message, this.peerId, +bubble.dataset.mid);
}); }
} else {
this.chat.appImManager.setInnerPeer({
peerId: peerId.toPeerId(),
lastMsgId: +mid
});
}
} else { } else {
const peerId = peerIdStr.toPeerId(); const peerId = peerIdStr.toPeerId();
if(peerId !== NULL_PEER_ID) { if(peerId !== NULL_PEER_ID) {

View File

@ -52,6 +52,7 @@ const icons = [
'mastercard', 'mastercard',
'visa', 'visa',
'unionpay', 'unionpay',
'mir',
'logo', 'logo',
]; ];
@ -102,7 +103,11 @@ export default class PopupPayment extends PopupElement {
private currency: string; private currency: string;
private tipButtonsMap: Map<number, HTMLElement>; private tipButtonsMap: Map<number, HTMLElement>;
constructor(private message: Message.message) { constructor(
private message: Message.message,
private receiptPeerId?: PeerId,
private receiptMsgId?: number
) {
super('popup-payment', { super('popup-payment', {
closable: true, closable: true,
overlayClosable: true, overlayClosable: true,
@ -153,7 +158,9 @@ export default class PopupPayment extends PopupElement {
const {message} = this; const {message} = this;
const mediaInvoice = message.media as MessageMedia.messageMediaInvoice; const mediaInvoice = message.media as MessageMedia.messageMediaInvoice;
_i18n(this.title, mediaInvoice.receipt_msg_id ? 'PaymentReceipt' : 'PaymentCheckout'); const isReceipt = !!(this.receiptMsgId || mediaInvoice.receipt_msg_id);
_i18n(this.title, isReceipt ? 'PaymentReceipt' : 'PaymentCheckout');
if(mediaInvoice.pFlags.test) { if(mediaInvoice.pFlags.test) {
this.title.append(' (Test)'); this.title.append(' (Test)');
} }
@ -170,7 +177,7 @@ export default class PopupPayment extends PopupElement {
let photoEl: HTMLElement; let photoEl: HTMLElement;
if(mediaInvoice.photo) { if(mediaInvoice.photo) {
photoEl = document.createElement('div'); photoEl = document.createElement('div');
photoEl.classList.add(detailsClassName + '-photo', 'media-container-cover'); photoEl.classList.add(detailsClassName + '-photo', 'media-container-contain');
wrapPhoto({ wrapPhoto({
photo: mediaInvoice.photo, photo: mediaInvoice.photo,
container: photoEl, container: photoEl,
@ -212,20 +219,22 @@ export default class PopupPayment extends PopupElement {
this.scrollable.container.append(preloaderContainer); this.scrollable.container.append(preloaderContainer);
let paymentForm: PaymentsPaymentForm | PaymentsPaymentReceipt; let paymentForm: PaymentsPaymentForm | PaymentsPaymentReceipt;
const isReceipt = !!mediaInvoice.receipt_msg_id;
this.receiptMsgId ??= mediaInvoice.receipt_msg_id;
this.receiptPeerId ??= this.receiptMsgId && message.peerId;
if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(message.peerId, mediaInvoice.receipt_msg_id); if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(this.receiptPeerId, this.receiptMsgId);
else paymentForm = await this.managers.appPaymentsManager.getPaymentForm(message.peerId, message.mid); else paymentForm = await this.managers.appPaymentsManager.getPaymentForm(message.peerId, message.mid);
let savedInfo = (paymentForm as PaymentsPaymentForm).saved_info || (paymentForm as PaymentsPaymentReceipt).info; let savedInfo = (paymentForm as PaymentsPaymentForm).saved_info || (paymentForm as PaymentsPaymentReceipt).info;
const savedCredentials = (paymentForm as PaymentsPaymentForm).saved_credentials; const savedCredentials = (paymentForm as PaymentsPaymentForm).saved_credentials;
let [lastRequestedInfo, passwordState, providerPeerTitle] = await Promise.all([ let [lastRequestedInfo, passwordState, providerPeerTitle] = await Promise.all([
!isReceipt && savedInfo && this.managers.appPaymentsManager.validateRequestedInfo(message.peerId, message.mid, savedInfo), !isReceipt && savedInfo && this.managers.appPaymentsManager.validateRequestedInfo(message.peerId, message.mid, savedInfo).catch(() => undefined),
savedCredentials && this.managers.passwordManager.getState(), savedCredentials && this.managers.passwordManager.getState(),
wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()}) wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()})
]); ]);
// console.log(paymentForm, lastRequestedInfo); console.log(paymentForm, lastRequestedInfo);
await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()}); await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()});
preloaderContainer.remove(); preloaderContainer.remove();
@ -258,8 +267,8 @@ export default class PopupPayment extends PopupElement {
const _label = makeLabel(); const _label = makeLabel();
_label.left.textContent = label; _label.left.textContent = label;
const wrappedAmount = wrapAmount(Math.abs(+amount)); const wrappedAmount = wrapAmount(amount);
_label.right.textContent = (amount < 0 ? '-' : '') + wrappedAmount; _label.right.textContent = wrappedAmount;
return _label.label; return _label.label;
}); });
@ -287,7 +296,7 @@ export default class PopupPayment extends PopupElement {
_i18n(totalLabel.left, 'PaymentTransactionTotal'); _i18n(totalLabel.left, 'PaymentTransactionTotal');
const totalAmount = accumulate(invoice.prices.map(({amount}) => +amount), 0); const totalAmount = accumulate(invoice.prices.map(({amount}) => +amount), 0);
const canTip = invoice.max_tip_amount !== undefined; const canTip = (invoice.max_tip_amount !== undefined && !isReceipt) || !!(paymentForm as PaymentsPaymentReceipt).tip_amount;
if(canTip) { if(canTip) {
const tipsClassName = className + '-tips'; const tipsClassName = className + '-tips';
@ -315,7 +324,7 @@ export default class PopupPayment extends PopupElement {
placeCaretAtEnd(input); placeCaretAtEnd(input);
} }
unsetActiveTip(); unsetActiveTip && unsetActiveTip();
const tipEl = this.tipButtonsMap.get(amount); const tipEl = this.tipButtonsMap.get(amount);
if(tipEl) { if(tipEl) {
tipEl.classList.add('active'); tipEl.classList.add('active');
@ -326,7 +335,7 @@ export default class PopupPayment extends PopupElement {
}; };
const tipsLabel = makeLabel(); const tipsLabel = makeLabel();
_i18n(tipsLabel.left, mediaInvoice.receipt_msg_id ? 'PaymentTip' : 'PaymentTipOptional'); _i18n(tipsLabel.left, isReceipt ? 'PaymentTip' : 'PaymentTipOptional');
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'tel'; input.type = 'tel';
// const input: HTMLElement = document.createElement('div'); // const input: HTMLElement = document.createElement('div');
@ -334,7 +343,12 @@ export default class PopupPayment extends PopupElement {
input.classList.add('input-clear', tipsClassName + '-input'); input.classList.add('input-clear', tipsClassName + '-input');
tipsLabel.right.append(input); tipsLabel.right.append(input);
tipsLabel.label.style.cursor = 'text'; if(!isReceipt) {
tipsLabel.label.style.cursor = 'text';
} else {
tipsLabel.label.classList.add('disable-hover');
}
tipsLabel.label.addEventListener('mousedown', (e) => { tipsLabel.label.addEventListener('mousedown', (e) => {
if(!findUpAsChild(e.target, input)) { if(!findUpAsChild(e.target, input)) {
placeCaretAtEnd(input); placeCaretAtEnd(input);
@ -384,53 +398,58 @@ export default class PopupPayment extends PopupElement {
pricesElements.push(tipsLabel.label); pricesElements.push(tipsLabel.label);
/// ///
const tipsEl = document.createElement('div'); let unsetActiveTip: () => void;
tipsEl.classList.add(tipsClassName); if(!isReceipt) {
const tipsEl = document.createElement('div');
const tipClassName = tipsClassName + '-tip'; tipsEl.classList.add(tipsClassName);
const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => {
const button = Button(tipClassName, {noRipple: true}); const tipClassName = tipsClassName + '-tip';
button.textContent = wrapAmount(tipAmount); const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => {
const button = Button(tipClassName, {noRipple: true});
this.tipButtonsMap.set(+tipAmount, button); button.textContent = wrapAmount(tipAmount);
return button;
}); this.tipButtonsMap.set(+tipAmount, button);
return button;
const unsetActiveTip = () => { });
const prevTipEl = tipsEl.querySelector('.active');
if(prevTipEl) { unsetActiveTip = () => {
prevTipEl.classList.remove('active'); const prevTipEl = tipsEl.querySelector('.active');
} if(prevTipEl) {
}; prevTipEl.classList.remove('active');
}
attachClickEvent(tipsEl, (e) => { };
const tipEl = findUpClassName(e.target, tipClassName);
if(!tipEl) { attachClickEvent(tipsEl, (e) => {
return; const tipEl = findUpClassName(e.target, tipClassName);
} if(!tipEl) {
return;
let tipAmount = 0; }
if(tipEl.classList.contains('active')) {
tipEl.classList.remove('active'); let tipAmount = 0;
} else { if(tipEl.classList.contains('active')) {
unsetActiveTip(); tipEl.classList.remove('active');
tipEl.classList.add('active'); } else {
unsetActiveTip();
for(const [amount, el] of this.tipButtonsMap) { tipEl.classList.add('active');
if(el === tipEl) {
tipAmount = amount; for(const [amount, el] of this.tipButtonsMap) {
break; if(el === tipEl) {
tipAmount = amount;
break;
}
} }
} }
}
setInputValue(tipAmount);
setInputValue(tipAmount); });
});
setInputValue(0);
setInputValue(0);
tipsEl.append(...tipButtons);
tipsEl.append(...tipButtons); pricesElements.push(tipsEl);
pricesElements.push(tipsEl); } else {
setInputValue((paymentForm as PaymentsPaymentReceipt).tip_amount);
}
} else { } else {
setTotal(); setTotal();
} }
@ -474,7 +493,7 @@ export default class PopupPayment extends PopupElement {
const setRowTitle = (row: Row, textContent: string) => { const setRowTitle = (row: Row, textContent: string) => {
row.title.textContent = textContent; row.title.textContent = textContent;
if(!textContent) { if(!textContent) {
const e = I18n.weakMap.get(row.subtitle) as I18n.IntlElement; const e = I18n.weakMap.get(row.subtitle.firstElementChild as HTMLElement) as I18n.IntlElement;
row.title.append(i18n(e.key)); row.title.append(i18n(e.key));
} }
@ -543,7 +562,8 @@ export default class PopupPayment extends PopupElement {
const postAddress = shippingAddress.shipping_address; const postAddress = shippingAddress.shipping_address;
setRowTitle(shippingAddressRow, [postAddress.city, postAddress.street_line1, postAddress.street_line2].filter(Boolean).join(', ')); setRowTitle(shippingAddressRow, [postAddress.city, postAddress.street_line1, postAddress.street_line2].filter(Boolean).join(', '));
shippingMethodRow.container.classList.remove('hide');
shippingMethodRow.container.classList.toggle('hide', !lastRequestedInfo && !isReceipt);
} : undefined; } : undefined;
const setShippingInfo = (info: PaymentRequestedInfo) => { const setShippingInfo = (info: PaymentRequestedInfo) => {
@ -586,7 +606,13 @@ export default class PopupPayment extends PopupElement {
shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0); shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0);
lastShippingPricesElements = makePricesElements(shippingOption.prices); lastShippingPricesElements = makePricesElements(shippingOption.prices);
let l = totalLabel.label; let l = totalLabel.label;
if(canTip) l = l.previousElementSibling.previousElementSibling as any; if(canTip) {
l = l.previousElementSibling as any;
if(!isReceipt) {
l = l.previousElementSibling as any;
}
}
lastShippingPricesElements.forEach((element) => l.parentElement.insertBefore(element, l)); lastShippingPricesElements.forEach((element) => l.parentElement.insertBefore(element, l));
setTotal(); setTotal();

View File

@ -23,6 +23,7 @@ import Row from "../row";
import { SettingSection } from "../sidebarLeft"; import { SettingSection } from "../sidebarLeft";
import { getPaymentBrandIconPath, PaymentButton, PaymentsCredentialsToken } from "./payment"; import { getPaymentBrandIconPath, PaymentButton, PaymentsCredentialsToken } from "./payment";
import { createVerificationIframe } from "./paymentVerification"; import { createVerificationIframe } from "./paymentVerification";
// import { putPreloader } from "../putPreloader";
export type PaymentCardDetails = { export type PaymentCardDetails = {
cardNumber: string; cardNumber: string;
@ -254,6 +255,7 @@ export default class PopupPaymentCard extends PopupElement<{
} }
}); });
// putPreloader(this.body, true);
this.body.append(iframe); this.body.append(iframe);
this.show(); this.show();
} }

View File

@ -264,7 +264,10 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage,
managers.appMessagesManager.fetchMessageReplyTo(message); managers.appMessagesManager.fetchMessageReplyTo(message);
} else { } else {
langPackKey = 'PaymentSuccessfullyPaid'; langPackKey = 'PaymentSuccessfullyPaid';
args.push(wrapLinkToMessage(invoiceMessage, plain)); args.push(wrapLinkToMessage(invoiceMessage, plain).then((el) => {
el.classList.add('is-receipt-link');
return el;
}));
} }
} }

View File

@ -3,14 +3,15 @@ import replaceNonNumber from "../string/replaceNonNumber";
const CARD_BRAND_REGEXP: {[brand: string]: RegExp} = { const CARD_BRAND_REGEXP: {[brand: string]: RegExp} = {
visa: /^4/, visa: /^4/,
mastercard: /^(51|52|53|54|55|22|23|24|25|26|27)/, mastercard: /^(51|52|53|54|55|222|23|24|25|26|27)/,
amex: /^(34|37)/, amex: /^(34|37)/,
discover: /^(60|64|65)/, discover: /^(60|64|65)/,
diners: /^(30|38|39)/, diners: /^(30|38|39)/,
diners14: /^(36)/, diners14: /^(36)/,
jcb: /^(35)/, jcb: /^(35)/,
unionpay: /^(62[0-6,8-9]|627[0-6,8-9]|6277[0-7,9]|62778[1-9]|81)/, unionpay: /^(62[0-6,8-9]|627[0-6,8-9]|6277[0-7,9]|62778[1-9]|81)/,
elo: /^(5067|509|636368|627780)/ elo: /^(5067|509|636368|627780)/,
mir: /^(220[0-4])/
}; };
// * taken from Stripe // * taken from Stripe
@ -74,6 +75,12 @@ export const CARD_BRANDS: {[b: string]: {
cvcMaxLength: 3, cvcMaxLength: 3,
cvcMinLength: null cvcMinLength: null
}, },
mir: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
unknown: { unknown: {
minLength: 16, minLength: 16,
maxLength: 16, maxLength: 16,

View File

@ -16,6 +16,7 @@ function makeValidationError(code?: string) {
} : null; } : null;
} }
// Luhn algorithm
function validateCompleteCardNumber(card: string) { function validateCompleteCardNumber(card: string) {
const t = '0'.charCodeAt(0); const t = '0'.charCodeAt(0);
const n = card.length % 2; const n = card.length % 2;
@ -61,7 +62,11 @@ function getCardInfoByNumber(card: string) {
} }
function makeCardNumberError(str: string, length: number, ignoreIncomplete: boolean) { function makeCardNumberError(str: string, length: number, ignoreIncomplete: boolean) {
return str.length >= length ? (validateCompleteCardNumber(str) ? null : makeValidationError('invalid')) : (ignoreIncomplete ? null : makeValidationError('incomplete')); if(str.length >= length) {
return validateCompleteCardNumber(str) || detectCardBrand(str) === 'mir' ? null : makeValidationError('invalid');
}
return ignoreIncomplete ? null : makeValidationError('incomplete');
} }
export function validateCardNumber(str: string, options: PatternValidationOptions = {}) { export function validateCardNumber(str: string, options: PatternValidationOptions = {}) {

View File

@ -25,49 +25,58 @@ function number_format(number: any, decimals: any, dec_point: any, thousands_sep
return s.join(dec); return s.join(dec);
} }
export default function paymentsWrapCurrencyAmount($amount: number | string, $currency: string, $skipSymbol?: boolean) { export default function paymentsWrapCurrencyAmount(amount: number | string, currency: string, skipSymbol?: boolean) {
$amount = +$amount; amount = +amount;
const $currency_data = Currencies[$currency]; // вытащить из json const isNegative = amount < 0;
if(!$currency_data) {
const currencyData = Currencies[currency];
if(!currencyData) {
throw new Error('CURRENCY_WRAP_INVALID'); throw new Error('CURRENCY_WRAP_INVALID');
} }
const $amount_exp = $amount / Math.pow(10, $currency_data['exp']); const amountExp = amount / Math.pow(10, currencyData.exp);
let $decimals = $currency_data['exp']; let decimals = currencyData.exp;
if($currency == 'IRR' && if(currency == 'IRR' && Math.floor(amountExp) == amountExp) {
Math.floor($amount_exp) == $amount_exp) { decimals = 0; // у иранцев копейки почти всегда = 0 и не показываются в UI
$decimals = 0; // у иранцев копейки почти всегда = 0 и не показываются в UI
} }
const $formatted = number_format($amount_exp, $decimals, $currency_data['decimal_sep'], $currency_data['thousands_sep']); let formatted = number_format(amountExp, decimals, currencyData.decimal_sep, currencyData.thousands_sep);
if($skipSymbol) { if(skipSymbol) {
return $formatted; return formatted;
} }
const $splitter = $currency_data['space_between'] ? " " : ''; let symbol = currencyData.symbol;
let $formatted_intern: string; if(isNegative && !currencyData.space_between && currencyData.symbol_left) {
if($currency_data['symbol_left']) { symbol = '-' + symbol;
$formatted_intern = $currency_data['symbol'] + $splitter + $formatted; formatted = formatted.replace('-', '');
}
let out: string;
const splitter = currencyData.space_between ? " " : '';
if(currencyData.symbol_left) {
out = symbol + splitter + formatted;
} else { } else {
$formatted_intern = $formatted + $splitter + $currency_data['symbol']; out = formatted + splitter + symbol;
} }
return $formatted_intern; return out;
} }
function paymentsGetCurrencyExp($currency: string) { (window as any).p = paymentsWrapCurrencyAmount;
if($currency == 'CLF') {
return 4; // function paymentsGetCurrencyExp($currency: string) {
} // if($currency == 'CLF') {
if(['BHD','IQD','JOD','KWD','LYD','OMR','TND'].includes($currency)) { // return 4;
return 3; // }
} // if(['BHD','IQD','JOD','KWD','LYD','OMR','TND'].includes($currency)) {
if(['BIF','BYR','CLP','CVE','DJF','GNF','ISK','JPY','KMF','KRW','MGA', 'PYG','RWF','UGX','UYI','VND','VUV','XAF','XOF','XPF'].includes($currency)) { // return 3;
return 0; // }
} // if(['BIF','BYR','CLP','CVE','DJF','GNF','ISK','JPY','KMF','KRW','MGA', 'PYG','RWF','UGX','UYI','VND','VUV','XAF','XOF','XPF'].includes($currency)) {
if($currency == 'MRO') { // return 0;
return 1; // }
} // if($currency == 'MRO') {
return 2; // return 1;
} // }
// return 2;
// }

View File

@ -2788,6 +2788,7 @@ $bubble-beside-button-width: 38px;
overflow: hidden; overflow: hidden;
min-height: 2.5rem; min-height: 2.5rem;
display: flex; display: flex;
border-radius: .375rem;
&:last-child { &:last-child {
border-bottom-left-radius: $border-radius-big; border-bottom-left-radius: $border-radius-big;
@ -2797,7 +2798,7 @@ $bubble-beside-button-width: 38px;
&-button { &-button {
padding: .5625rem 0; padding: .5625rem 0;
border-radius: 6px; border-radius: inherit;
z-index: 2; z-index: 2;
font-size: .875rem; font-size: .875rem;
user-select: none; user-select: none;

View File

@ -82,7 +82,8 @@
.payment-verification { .payment-verification {
width: 100%; width: 100%;
min-height: 30rem; height: 40rem;
max-height: 100%;
border: none; border: none;
flex: 1 1 auto; flex: 1 1 auto;
} }

View File

@ -1268,6 +1268,16 @@ middle-ellipsis-element {
// } // }
// } // }
.media-container-contain {
position: relative;
.media-photo {
object-fit: contain;
width: 100%;
height: 100%;
}
}
.media-container-cover { .media-container-cover {
position: relative; position: relative;