Browse Source

Payments fixes

master
Eduard Kuzmenko 2 years ago
parent
commit
2c9d5c30fe
  1. 16
      src/components/chat/bubbles.ts
  2. 148
      src/components/popups/payment.ts
  3. 2
      src/components/popups/paymentCard.ts
  4. 5
      src/components/wrappers/messageActionTextNewUnsafe.ts
  5. 11
      src/helpers/cards/cardBrands.ts
  6. 7
      src/helpers/cards/validateCard.ts
  7. 77
      src/helpers/paymentsWrapCurrencyAmount.ts
  8. 3
      src/scss/partials/_chatBubble.scss
  9. 3
      src/scss/partials/popups/_payment.scss
  10. 10
      src/scss/style.scss

16
src/components/chat/bubbles.ts

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

148
src/components/popups/payment.ts

@ -52,6 +52,7 @@ const icons = [ @@ -52,6 +52,7 @@ const icons = [
'mastercard',
'visa',
'unionpay',
'mir',
'logo',
];
@ -102,7 +103,11 @@ export default class PopupPayment extends PopupElement { @@ -102,7 +103,11 @@ export default class PopupPayment extends PopupElement {
private currency: string;
private tipButtonsMap: Map<number, HTMLElement>;
constructor(private message: Message.message) {
constructor(
private message: Message.message,
private receiptPeerId?: PeerId,
private receiptMsgId?: number
) {
super('popup-payment', {
closable: true,
overlayClosable: true,
@ -153,7 +158,9 @@ export default class PopupPayment extends PopupElement { @@ -153,7 +158,9 @@ export default class PopupPayment extends PopupElement {
const {message} = this;
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) {
this.title.append(' (Test)');
}
@ -170,7 +177,7 @@ export default class PopupPayment extends PopupElement { @@ -170,7 +177,7 @@ export default class PopupPayment extends PopupElement {
let photoEl: HTMLElement;
if(mediaInvoice.photo) {
photoEl = document.createElement('div');
photoEl.classList.add(detailsClassName + '-photo', 'media-container-cover');
photoEl.classList.add(detailsClassName + '-photo', 'media-container-contain');
wrapPhoto({
photo: mediaInvoice.photo,
container: photoEl,
@ -212,20 +219,22 @@ export default class PopupPayment extends PopupElement { @@ -212,20 +219,22 @@ export default class PopupPayment extends PopupElement {
this.scrollable.container.append(preloaderContainer);
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);
let savedInfo = (paymentForm as PaymentsPaymentForm).saved_info || (paymentForm as PaymentsPaymentReceipt).info;
const savedCredentials = (paymentForm as PaymentsPaymentForm).saved_credentials;
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(),
wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()})
]);
// console.log(paymentForm, lastRequestedInfo);
console.log(paymentForm, lastRequestedInfo);
await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()});
preloaderContainer.remove();
@ -258,8 +267,8 @@ export default class PopupPayment extends PopupElement { @@ -258,8 +267,8 @@ export default class PopupPayment extends PopupElement {
const _label = makeLabel();
_label.left.textContent = label;
const wrappedAmount = wrapAmount(Math.abs(+amount));
_label.right.textContent = (amount < 0 ? '-' : '') + wrappedAmount;
const wrappedAmount = wrapAmount(amount);
_label.right.textContent = wrappedAmount;
return _label.label;
});
@ -287,7 +296,7 @@ export default class PopupPayment extends PopupElement { @@ -287,7 +296,7 @@ export default class PopupPayment extends PopupElement {
_i18n(totalLabel.left, 'PaymentTransactionTotal');
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) {
const tipsClassName = className + '-tips';
@ -315,7 +324,7 @@ export default class PopupPayment extends PopupElement { @@ -315,7 +324,7 @@ export default class PopupPayment extends PopupElement {
placeCaretAtEnd(input);
}
unsetActiveTip();
unsetActiveTip && unsetActiveTip();
const tipEl = this.tipButtonsMap.get(amount);
if(tipEl) {
tipEl.classList.add('active');
@ -326,7 +335,7 @@ export default class PopupPayment extends PopupElement { @@ -326,7 +335,7 @@ export default class PopupPayment extends PopupElement {
};
const tipsLabel = makeLabel();
_i18n(tipsLabel.left, mediaInvoice.receipt_msg_id ? 'PaymentTip' : 'PaymentTipOptional');
_i18n(tipsLabel.left, isReceipt ? 'PaymentTip' : 'PaymentTipOptional');
const input = document.createElement('input');
input.type = 'tel';
// const input: HTMLElement = document.createElement('div');
@ -334,7 +343,12 @@ export default class PopupPayment extends PopupElement { @@ -334,7 +343,12 @@ export default class PopupPayment extends PopupElement {
input.classList.add('input-clear', tipsClassName + '-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) => {
if(!findUpAsChild(e.target, input)) {
placeCaretAtEnd(input);
@ -384,53 +398,58 @@ export default class PopupPayment extends PopupElement { @@ -384,53 +398,58 @@ export default class PopupPayment extends PopupElement {
pricesElements.push(tipsLabel.label);
///
const tipsEl = document.createElement('div');
tipsEl.classList.add(tipsClassName);
const tipClassName = tipsClassName + '-tip';
const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => {
const button = Button(tipClassName, {noRipple: true});
button.textContent = wrapAmount(tipAmount);
this.tipButtonsMap.set(+tipAmount, button);
return button;
});
const unsetActiveTip = () => {
const prevTipEl = tipsEl.querySelector('.active');
if(prevTipEl) {
prevTipEl.classList.remove('active');
}
};
attachClickEvent(tipsEl, (e) => {
const tipEl = findUpClassName(e.target, tipClassName);
if(!tipEl) {
return;
}
let tipAmount = 0;
if(tipEl.classList.contains('active')) {
tipEl.classList.remove('active');
} else {
unsetActiveTip();
tipEl.classList.add('active');
for(const [amount, el] of this.tipButtonsMap) {
if(el === tipEl) {
tipAmount = amount;
break;
let unsetActiveTip: () => void;
if(!isReceipt) {
const tipsEl = document.createElement('div');
tipsEl.classList.add(tipsClassName);
const tipClassName = tipsClassName + '-tip';
const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => {
const button = Button(tipClassName, {noRipple: true});
button.textContent = wrapAmount(tipAmount);
this.tipButtonsMap.set(+tipAmount, button);
return button;
});
unsetActiveTip = () => {
const prevTipEl = tipsEl.querySelector('.active');
if(prevTipEl) {
prevTipEl.classList.remove('active');
}
};
attachClickEvent(tipsEl, (e) => {
const tipEl = findUpClassName(e.target, tipClassName);
if(!tipEl) {
return;
}
let tipAmount = 0;
if(tipEl.classList.contains('active')) {
tipEl.classList.remove('active');
} else {
unsetActiveTip();
tipEl.classList.add('active');
for(const [amount, el] of this.tipButtonsMap) {
if(el === tipEl) {
tipAmount = amount;
break;
}
}
}
}
setInputValue(tipAmount);
});
setInputValue(0);
tipsEl.append(...tipButtons);
pricesElements.push(tipsEl);
setInputValue(tipAmount);
});
setInputValue(0);
tipsEl.append(...tipButtons);
pricesElements.push(tipsEl);
} else {
setInputValue((paymentForm as PaymentsPaymentReceipt).tip_amount);
}
} else {
setTotal();
}
@ -474,7 +493,7 @@ export default class PopupPayment extends PopupElement { @@ -474,7 +493,7 @@ export default class PopupPayment extends PopupElement {
const setRowTitle = (row: Row, textContent: string) => {
row.title.textContent = 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));
}
@ -543,7 +562,8 @@ export default class PopupPayment extends PopupElement { @@ -543,7 +562,8 @@ export default class PopupPayment extends PopupElement {
const postAddress = shippingAddress.shipping_address;
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;
const setShippingInfo = (info: PaymentRequestedInfo) => {
@ -586,7 +606,13 @@ export default class PopupPayment extends PopupElement { @@ -586,7 +606,13 @@ export default class PopupPayment extends PopupElement {
shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0);
lastShippingPricesElements = makePricesElements(shippingOption.prices);
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));
setTotal();

2
src/components/popups/paymentCard.ts

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

5
src/components/wrappers/messageActionTextNewUnsafe.ts

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

11
src/helpers/cards/cardBrands.ts

@ -3,14 +3,15 @@ import replaceNonNumber from "../string/replaceNonNumber"; @@ -3,14 +3,15 @@ import replaceNonNumber from "../string/replaceNonNumber";
const CARD_BRAND_REGEXP: {[brand: string]: RegExp} = {
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)/,
discover: /^(60|64|65)/,
diners: /^(30|38|39)/,
diners14: /^(36)/,
jcb: /^(35)/,
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
@ -74,6 +75,12 @@ export const CARD_BRANDS: {[b: string]: { @@ -74,6 +75,12 @@ export const CARD_BRANDS: {[b: string]: {
cvcMaxLength: 3,
cvcMinLength: null
},
mir: {
minLength: 16,
maxLength: 16,
cvcMaxLength: 3,
cvcMinLength: null
},
unknown: {
minLength: 16,
maxLength: 16,

7
src/helpers/cards/validateCard.ts

@ -16,6 +16,7 @@ function makeValidationError(code?: string) { @@ -16,6 +16,7 @@ function makeValidationError(code?: string) {
} : null;
}
// Luhn algorithm
function validateCompleteCardNumber(card: string) {
const t = '0'.charCodeAt(0);
const n = card.length % 2;
@ -61,7 +62,11 @@ function getCardInfoByNumber(card: string) { @@ -61,7 +62,11 @@ function getCardInfoByNumber(card: string) {
}
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 = {}) {

77
src/helpers/paymentsWrapCurrencyAmount.ts

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

3
src/scss/partials/_chatBubble.scss

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

3
src/scss/partials/popups/_payment.scss

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

10
src/scss/style.scss

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

Loading…
Cancel
Save