Payments
This commit is contained in:
parent
d916eb17ea
commit
86c7640f13
src
components
call
chat
confirmationPopup.tscountryInputField.tsgroupCall
inputField.tsinputFieldAnimated.tsmiddleEllipsis.tspopups
avatar.tscreateContact.tscreatePoll.tsdatePicker.tsindex.tsjoinChatInvite.tsmute.tsnewMedia.tspayment.tspaymentCard.tspaymentCardConfirmation.tspaymentShipping.tspaymentShippingMethods.tspaymentVerification.tspeer.tspickUser.tsreactedList.tsschedule.tssponsored.tsstickers.ts
sidebarLeft
index.ts
usernameInputField.tstabs
wrappers
config
helpers
array
cacheCallback.tscards
cardBrands.tscardFormattingPatterns.tsformatInputValueByPattern.tsformatValueByPattern.tspatternCharacters.tsvalidateCard.ts
dom
long
paymentsWrapCurrencyAmount.tsscrollSaver.tsstring
lib
appManagers
appImManager.tsappInlineBotsManager.tsappMessagesManager.tsappPaymentsManager.tsappPeersManager.tscreateManagers.tsmanager.tsuiNotificationsManager.ts
mtproto
rootScope.tspages
scss
components
mixins
partials
style.scsstgico
tests
types.d.ts@ -77,7 +77,7 @@ export default class PopupCall extends PopupElement {
|
||||
private controlsHover: ControlsHover;
|
||||
|
||||
constructor(private instance: CallInstance) {
|
||||
super('popup-call', undefined, {
|
||||
super('popup-call', {
|
||||
withoutOverlay: true,
|
||||
closable: true
|
||||
});
|
||||
|
@ -109,6 +109,7 @@ import { EmoticonsDropdown } from "../emoticonsDropdown";
|
||||
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
|
||||
import noop from "../../helpers/noop";
|
||||
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
|
||||
import PopupPayment from "../popups/payment";
|
||||
|
||||
const USE_MEDIA_TAILS = false;
|
||||
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
|
||||
@ -540,12 +541,15 @@ export default class ChatBubbles {
|
||||
});
|
||||
});
|
||||
|
||||
this.listenerSetter.add(rootScope)('message_edit', ({storageKey, message}) => {
|
||||
this.listenerSetter.add(rootScope)('message_edit', async({storageKey, message}) => {
|
||||
if(storageKey !== this.chat.messagesStorageKey) return;
|
||||
|
||||
const bubble = this.bubbles[message.mid];
|
||||
if(!bubble) return;
|
||||
|
||||
await getHeavyAnimationPromise();
|
||||
if(this.bubbles[message.mid] !== bubble) return;
|
||||
|
||||
this.safeRenderMessage(message, true, bubble);
|
||||
});
|
||||
|
||||
@ -1467,6 +1471,18 @@ export default class ChatBubbles {
|
||||
return;
|
||||
}
|
||||
|
||||
const buyButton: HTMLElement = findUpClassName(target, 'is-buy');
|
||||
if(buyButton) {
|
||||
const message = await this.chat.getMessage(+bubble.dataset.mid);
|
||||
if(!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
new PopupPayment(message as Message.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const spoiler: HTMLElement = findUpClassName(target, 'spoiler');
|
||||
if(spoiler) {
|
||||
const messageDiv = findUpClassName(spoiler, 'message');
|
||||
@ -1898,8 +1914,6 @@ export default class ChatBubbles {
|
||||
}
|
||||
|
||||
public loadMoreHistory(top: boolean, justLoad = false) {
|
||||
// return;
|
||||
|
||||
//this.log('loadMoreHistory', top);
|
||||
if(
|
||||
!this.peerId ||
|
||||
@ -3081,6 +3095,10 @@ export default class ChatBubbles {
|
||||
const processQueue = async(): Promise<void> => {
|
||||
log('start');
|
||||
|
||||
// if(!this.chat.setPeerPromise) {
|
||||
// await pause(10000000);
|
||||
// }
|
||||
|
||||
const renderQueue = this.messagesQueue.slice();
|
||||
this.messagesQueue.length = 0;
|
||||
|
||||
@ -3164,7 +3182,9 @@ export default class ChatBubbles {
|
||||
|
||||
this.ejectBubbles();
|
||||
for(const [bubble, oldBubble] of this.bubblesToReplace) {
|
||||
scrollSaver.replaceSaved(oldBubble, bubble);
|
||||
if(scrollSaver) {
|
||||
scrollSaver.replaceSaved(oldBubble, bubble);
|
||||
}
|
||||
|
||||
if(!loadQueue.find((details) => details.bubble === bubble)) {
|
||||
continue;
|
||||
@ -3674,7 +3694,12 @@ export default class ChatBubbles {
|
||||
let target = e.target as HTMLElement;
|
||||
|
||||
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
|
||||
if(!target || target.classList.contains('is-link') || target.classList.contains('is-switch-inline')) return;
|
||||
if(
|
||||
!target
|
||||
|| target.classList.contains('is-link')
|
||||
|| target.classList.contains('is-switch-inline')
|
||||
|| target.classList.contains('is-buy')
|
||||
) return;
|
||||
|
||||
cancelEvent(e);
|
||||
|
||||
@ -4235,12 +4260,13 @@ export default class ChatBubbles {
|
||||
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.classList.add('bubble-primary-color');
|
||||
setInnerHTML(titleDiv, wrapRichText(messageMedia.title));
|
||||
setInnerHTML(titleDiv, wrapEmojiText(messageMedia.title));
|
||||
|
||||
const richText = wrapRichText(messageMedia.description);
|
||||
const richText = wrapEmojiText(messageMedia.description);
|
||||
messageDiv.prepend(...[titleDiv, !photo && priceEl, richText].filter(Boolean));
|
||||
|
||||
bubble.classList.remove('is-message-empty');
|
||||
bubble.classList.add('is-invoice');
|
||||
|
||||
break;
|
||||
}
|
||||
@ -4943,7 +4969,7 @@ export default class ChatBubbles {
|
||||
const isSponsored = !!(message as Message.message).pFlags.sponsored;
|
||||
const middleware = this.getMiddleware();
|
||||
const m = middlewarePromise(middleware);
|
||||
return this.safeRenderMessage(message, isSponsored ? false : true, undefined, false, async(result) => {
|
||||
return this.safeRenderMessage(message, isSponsored ? false : true, undefined, isSponsored, async(result) => {
|
||||
const {bubble} = await m(result);
|
||||
if(!bubble) {
|
||||
return result;
|
||||
|
@ -93,6 +93,7 @@ import { emojiFromCodePoints } from "../../vendor/emoji";
|
||||
import { modifyAckedPromise } from "../../helpers/modifyAckedResult";
|
||||
import ChatSendAs from "./sendAs";
|
||||
import filterAsync from "../../helpers/array/filterAsync";
|
||||
import InputFieldAnimated from "../inputFieldAnimated";
|
||||
|
||||
const RECORD_MIN_TIME = 500;
|
||||
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
|
||||
@ -103,7 +104,7 @@ export default class ChatInput {
|
||||
// private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
|
||||
private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|^[^:@\/])(?!.*[:@\/]).*)$/;
|
||||
public messageInput: HTMLElement;
|
||||
public messageInputField: InputField;
|
||||
public messageInputField: InputFieldAnimated;
|
||||
private fileInput: HTMLInputElement;
|
||||
private inputMessageContainer: HTMLDivElement;
|
||||
private btnSend: HTMLButtonElement;
|
||||
@ -1441,10 +1442,10 @@ export default class ChatInput {
|
||||
|
||||
private attachMessageInputField() {
|
||||
const oldInputField = this.messageInputField;
|
||||
this.messageInputField = new InputField({
|
||||
this.messageInputField = new InputFieldAnimated({
|
||||
placeholder: 'Message',
|
||||
name: 'message',
|
||||
animate: true
|
||||
withLinebreaks: true
|
||||
});
|
||||
|
||||
this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
|
||||
@ -1907,7 +1908,7 @@ export default class ChatInput {
|
||||
//const saveExecuted = this.prepareDocumentExecute();
|
||||
// can't exec .value here because it will instantly check for autocomplete
|
||||
const value = documentFragmentToHTML(wrapDraftText(newValue, {entities}));
|
||||
this.messageInputField.setValueSilently(value, true);
|
||||
this.messageInputField.setValueSilently(value);
|
||||
|
||||
const caret = this.messageInput.querySelector('.composer-sel');
|
||||
if(caret) {
|
||||
|
@ -8,7 +8,7 @@ import { addCancelButton } from "./popups";
|
||||
import PopupPeer, { PopupPeerOptions } from "./popups/peer";
|
||||
|
||||
// type PopupConfirmationOptions = Pick<PopupPeerOptions, 'titleLangKey'>;
|
||||
type PopupConfirmationOptions = PopupPeerOptions & {
|
||||
export type PopupConfirmationOptions = PopupPeerOptions & {
|
||||
button: PopupPeerOptions['buttons'][0],
|
||||
checkbox?: PopupPeerOptions['checkboxes'][0]
|
||||
};
|
||||
@ -20,14 +20,14 @@ export default function confirmationPopup(options: PopupConfirmationOptions) {
|
||||
resolve(set ? !!set.size : undefined);
|
||||
};
|
||||
|
||||
const buttons = addCancelButton([button]);
|
||||
const buttons = addCancelButton(options.buttons || [button]);
|
||||
const cancelButton = buttons.find((button) => button.isCancel);
|
||||
cancelButton.callback = () => {
|
||||
reject();
|
||||
};
|
||||
|
||||
options.buttons = buttons;
|
||||
options.checkboxes = checkbox && [checkbox];
|
||||
options.checkboxes ??= checkbox && [checkbox];
|
||||
|
||||
new PopupPeer('popup-confirmation', options).show();
|
||||
});
|
||||
|
288
src/components/countryInputField.ts
Normal file
288
src/components/countryInputField.ts
Normal file
@ -0,0 +1,288 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import IS_EMOJI_SUPPORTED from "../environment/emojiSupport";
|
||||
import cancelEvent from "../helpers/dom/cancelEvent";
|
||||
import simulateEvent from "../helpers/dom/dispatchEvent";
|
||||
import findUpClassName from "../helpers/dom/findUpClassName";
|
||||
import findUpTag from "../helpers/dom/findUpTag";
|
||||
import replaceContent from "../helpers/dom/replaceContent";
|
||||
import setInnerHTML from "../helpers/dom/setInnerHTML";
|
||||
import fastSmoothScroll from "../helpers/fastSmoothScroll";
|
||||
import { randomLong } from "../helpers/random";
|
||||
import { HelpCountry, HelpCountryCode } from "../layer";
|
||||
import I18n, { i18n } from "../lib/langPack";
|
||||
import wrapEmojiText from "../lib/richTextProcessor/wrapEmojiText";
|
||||
import rootScope from "../lib/rootScope";
|
||||
import { getCountryEmoji } from "../vendor/emoji";
|
||||
import InputField, { InputFieldOptions } from "./inputField";
|
||||
import Scrollable from "./scrollable";
|
||||
|
||||
let countries: HelpCountry.helpCountry[];
|
||||
const setCountries = () => {
|
||||
countries = I18n.countriesList
|
||||
.filter((country) => !country.pFlags?.hidden)
|
||||
.sort((a, b) => (a.name || a.default_name).localeCompare(b.name || b.default_name));
|
||||
};
|
||||
|
||||
let init = () => {
|
||||
setCountries();
|
||||
rootScope.addEventListener('language_change', () => {
|
||||
setCountries();
|
||||
});
|
||||
};
|
||||
|
||||
export default class CountryInputField extends InputField {
|
||||
private lastCountrySelected: HelpCountry;
|
||||
private lastCountryCodeSelected: HelpCountryCode;
|
||||
|
||||
private hideTimeout: number;
|
||||
private selectWrapper: HTMLElement;
|
||||
|
||||
private liMap: Map<string, HTMLLIElement[]>;
|
||||
|
||||
constructor(public options: InputFieldOptions & {
|
||||
onCountryChange?: (country: HelpCountry.helpCountry, code: HelpCountryCode.helpCountryCode) => void,
|
||||
noPhoneCodes?: boolean
|
||||
} = {}) {
|
||||
super({
|
||||
label: 'Country',
|
||||
name: randomLong(),
|
||||
...options,
|
||||
});
|
||||
|
||||
if(init) {
|
||||
init();
|
||||
init = undefined;
|
||||
}
|
||||
|
||||
this.liMap = new Map();
|
||||
|
||||
this.container.classList.add('input-select');
|
||||
|
||||
const selectWrapper = this.selectWrapper = document.createElement('div');
|
||||
selectWrapper.classList.add('select-wrapper', 'z-depth-3', 'hide');
|
||||
|
||||
const arrowDown = document.createElement('span');
|
||||
arrowDown.classList.add('arrow', 'arrow-down');
|
||||
this.container.append(arrowDown);
|
||||
|
||||
const selectList = document.createElement('ul');
|
||||
selectWrapper.appendChild(selectList);
|
||||
|
||||
const scroll = new Scrollable(selectWrapper);
|
||||
|
||||
let initSelect = () => {
|
||||
initSelect = null;
|
||||
|
||||
countries.forEach((c) => {
|
||||
const emoji = getCountryEmoji(c.iso2);
|
||||
|
||||
const liArr: Array<HTMLLIElement> = [];
|
||||
for(let i = 0, length = Math.min(c.country_codes.length, options.noPhoneCodes ? 1 : Infinity); i < length; ++i) {
|
||||
const countryCode = c.country_codes[i];
|
||||
const li = document.createElement('li');
|
||||
|
||||
let wrapped = wrapEmojiText(emoji);
|
||||
if(IS_EMOJI_SUPPORTED) {
|
||||
const spanEmoji = document.createElement('span');
|
||||
setInnerHTML(spanEmoji, wrapped);
|
||||
li.append(spanEmoji);
|
||||
} else {
|
||||
setInnerHTML(li, wrapped);
|
||||
}
|
||||
|
||||
const el = i18n(c.default_name as any);
|
||||
el.dataset.defaultName = c.default_name;
|
||||
li.append(el);
|
||||
|
||||
if(!options.noPhoneCodes) {
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('phone-code');
|
||||
span.innerText = '+' + countryCode.country_code;
|
||||
li.appendChild(span);
|
||||
}
|
||||
|
||||
liArr.push(li);
|
||||
selectList.append(li);
|
||||
}
|
||||
|
||||
this.liMap.set(c.iso2, liArr);
|
||||
});
|
||||
|
||||
selectList.addEventListener('mousedown', (e) => {
|
||||
if(e.button !== 0) { // other buttons but left shall not pass
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findUpTag(e.target, 'LI')
|
||||
this.selectCountryByTarget(target);
|
||||
//console.log('clicked', e, countryName, phoneCode);
|
||||
});
|
||||
|
||||
this.container.appendChild(selectWrapper);
|
||||
};
|
||||
|
||||
initSelect();
|
||||
|
||||
this.input.addEventListener('focus', (e) => {
|
||||
if(initSelect) {
|
||||
initSelect();
|
||||
} else {
|
||||
countries.forEach((c) => {
|
||||
this.liMap.get(c.iso2).forEach((li) => li.style.display = '');
|
||||
});
|
||||
}
|
||||
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = undefined;
|
||||
|
||||
selectWrapper.classList.remove('hide');
|
||||
void selectWrapper.offsetWidth; // reflow
|
||||
selectWrapper.classList.add('active');
|
||||
|
||||
this.select();
|
||||
|
||||
fastSmoothScroll({
|
||||
// container: page.pageEl.parentElement.parentElement,
|
||||
container: findUpClassName(this.container, 'scrollable-y'),
|
||||
element: this.input,
|
||||
position: 'start',
|
||||
margin: 4
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if(!mouseDownHandlerAttached) {
|
||||
document.addEventListener('mousedown', onMouseDown, {capture: true});
|
||||
mouseDownHandlerAttached = true;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
let mouseDownHandlerAttached = false;
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if(findUpClassName(e.target, 'input-select')) {
|
||||
return;
|
||||
}
|
||||
if(e.target === this.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hidePicker();
|
||||
document.removeEventListener('mousedown', onMouseDown, {capture: true});
|
||||
mouseDownHandlerAttached = false;
|
||||
};
|
||||
|
||||
/* false && this.input.addEventListener('blur', function(this: typeof this.input, e) {
|
||||
hidePicker();
|
||||
|
||||
e.cancelBubble = true;
|
||||
}, {capture: true}); */
|
||||
|
||||
const onKeyPress = (e: KeyboardEvent) => {
|
||||
const key = e.key;
|
||||
if(e.ctrlKey || key === 'Control') return false;
|
||||
|
||||
//let i = new RegExp('^' + this.value, 'i');
|
||||
let _value = this.value.toLowerCase();
|
||||
let matches: HelpCountry[] = [];
|
||||
countries.forEach((c) => {
|
||||
const names = [
|
||||
c.name,
|
||||
c.default_name,
|
||||
c.iso2
|
||||
];
|
||||
|
||||
names.filter(Boolean).forEach((name) => {
|
||||
const abbr = name.split(' ').filter((word) => /\w/.test(word)).map((word) => word[0]).join('');
|
||||
if(abbr.length > 1) {
|
||||
names.push(abbr);
|
||||
}
|
||||
});
|
||||
|
||||
let good = !!names.filter(Boolean).find((str) => str.toLowerCase().indexOf(_value) !== -1)/* === 0 */;//i.test(c.name);
|
||||
|
||||
this.liMap.get(c.iso2).forEach((li) => li.style.display = good ? '' : 'none');
|
||||
if(good) matches.push(c);
|
||||
});
|
||||
|
||||
// Код ниже автоматически выберет страну если она осталась одна при поиске
|
||||
/* if(matches.length === 1 && matches[0].li.length === 1) {
|
||||
if(matches[0].name === lastCountrySelected) return false;
|
||||
//console.log('clicking', matches[0]);
|
||||
|
||||
var clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initEvent('mousedown', true, true);
|
||||
matches[0].li[0].dispatchEvent(clickEvent);
|
||||
return false;
|
||||
} else */if(matches.length === 0) {
|
||||
countries.forEach((c) => {
|
||||
this.liMap.get(c.iso2).forEach((li) => li.style.display = '');
|
||||
});
|
||||
} else if(matches.length === 1 && key === 'Enter') {
|
||||
cancelEvent(e);
|
||||
this.selectCountryByTarget(this.liMap.get(matches[0].iso2)[0]);
|
||||
}
|
||||
};
|
||||
|
||||
this.input.addEventListener('keyup', onKeyPress);
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if(e.key === 'Enter') {
|
||||
onKeyPress(e);
|
||||
}
|
||||
});
|
||||
|
||||
arrowDown.addEventListener('mousedown', (e) => {
|
||||
if(this.input.matches(':focus')) {
|
||||
this.hidePicker();
|
||||
this.input.blur();
|
||||
} else {
|
||||
e.cancelBubble = true;
|
||||
e.preventDefault();
|
||||
this.input.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getSelected() {
|
||||
return {country: this.lastCountrySelected, code: this.lastCountryCodeSelected};
|
||||
}
|
||||
|
||||
public hidePicker = () => {
|
||||
if(this.hideTimeout !== undefined) return;
|
||||
this.selectWrapper.classList.remove('active');
|
||||
this.hideTimeout = window.setTimeout(() => {
|
||||
this.selectWrapper.classList.add('hide');
|
||||
this.hideTimeout = undefined;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
public selectCountryByTarget = (target: HTMLElement) => {
|
||||
const defaultName = target.querySelector<HTMLElement>('[data-default-name]').dataset.defaultName;
|
||||
const phoneCodeEl = target.querySelector<HTMLElement>('.phone-code');
|
||||
const phoneCode = phoneCodeEl?.innerText;
|
||||
const countryCode = phoneCode && phoneCode.replace(/\D/g, '');
|
||||
|
||||
replaceContent(this.input, i18n(defaultName as any));
|
||||
simulateEvent(this.input, 'input');
|
||||
this.lastCountrySelected = countries.find((c) => c.default_name === defaultName);
|
||||
this.lastCountryCodeSelected = countryCode && this.lastCountrySelected.country_codes.find((_countryCode) => _countryCode.country_code === countryCode);
|
||||
|
||||
this.options.onCountryChange?.(this.lastCountrySelected, this.lastCountryCodeSelected);
|
||||
this.hidePicker();
|
||||
}
|
||||
|
||||
public selectCountryByIso2(iso2: string) {
|
||||
this.selectCountryByTarget(this.liMap.get(iso2)[0]);
|
||||
}
|
||||
|
||||
public override(country: HelpCountry, code: HelpCountryCode, countryName?: string) {
|
||||
replaceContent(this.input, country ? i18n(country.default_name as any) : countryName);
|
||||
this.lastCountrySelected = country;
|
||||
this.lastCountryCodeSelected = code;
|
||||
this.options.onCountryChange?.(this.lastCountrySelected, this.lastCountryCodeSelected);
|
||||
}
|
||||
}
|
@ -140,10 +140,11 @@ export default class PopupGroupCall extends PopupElement {
|
||||
private btnScreen: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
super('popup-group-call', undefined, {
|
||||
super('popup-group-call', {
|
||||
body: true,
|
||||
withoutOverlay: true,
|
||||
closable: true
|
||||
closable: true,
|
||||
title: true
|
||||
});
|
||||
|
||||
this.videosCount = 0;
|
||||
|
@ -4,6 +4,7 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import cancelEvent from "../helpers/dom/cancelEvent";
|
||||
import simulateEvent from "../helpers/dom/dispatchEvent";
|
||||
import documentFragmentToHTML from "../helpers/dom/documentFragmentToHTML";
|
||||
import findUpAttribute from "../helpers/dom/findUpAttribute";
|
||||
@ -16,14 +17,15 @@ import { i18n, LangPackKey, _i18n } from "../lib/langPack";
|
||||
import mergeEntities from "../lib/richTextProcessor/mergeEntities";
|
||||
import parseEntities from "../lib/richTextProcessor/parseEntities";
|
||||
import wrapDraftText from "../lib/richTextProcessor/wrapDraftText";
|
||||
import SetTransition from "./singleTransition";
|
||||
|
||||
let init = () => {
|
||||
document.addEventListener('paste', (e) => {
|
||||
if(!findUpAttribute(e.target, 'contenteditable="true"')) {
|
||||
const input = findUpAttribute(e.target, 'contenteditable="true"');
|
||||
if(!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noLinebreaks = !!input.dataset.noLinebreaks;
|
||||
e.preventDefault();
|
||||
let text: string, entities: MessageEntity[];
|
||||
|
||||
@ -33,6 +35,14 @@ let init = () => {
|
||||
|
||||
// @ts-ignore
|
||||
let html: string = (e.originalEvent || e).clipboardData.getData('text/html');
|
||||
|
||||
const filterEntity = (e: MessageEntity) => e._ === 'messageEntityEmoji' || (e._ === 'messageEntityLinebreak' && !noLinebreaks);
|
||||
if(noLinebreaks) {
|
||||
const regExp = /[\r\n]/g;
|
||||
plainText = plainText.replace(regExp, '');
|
||||
html = html.replace(regExp, '');
|
||||
}
|
||||
|
||||
if(html.trim()) {
|
||||
html = html.replace(/<style([\s\S]*)<\/style>/, '');
|
||||
html = html.replace(/<!--([\s\S]*)-->/, '');
|
||||
@ -64,7 +74,7 @@ let init = () => {
|
||||
usePlainText = false;
|
||||
|
||||
let entities2 = parseEntities(text);
|
||||
entities2 = entities2.filter((e) => e._ === 'messageEntityEmoji' || e._ === 'messageEntityLinebreak');
|
||||
entities2 = entities2.filter(filterEntity);
|
||||
mergeEntities(entities, entities2);
|
||||
}
|
||||
}
|
||||
@ -72,7 +82,7 @@ let init = () => {
|
||||
if(usePlainText) {
|
||||
text = plainText;
|
||||
entities = parseEntities(text);
|
||||
entities = entities.filter((e) => e._ === 'messageEntityEmoji' || e._ === 'messageEntityLinebreak');
|
||||
entities = entities.filter(filterEntity);
|
||||
}
|
||||
|
||||
const fragment = wrapDraftText(text, {entities});
|
||||
@ -116,16 +126,17 @@ export type InputFieldOptions = {
|
||||
maxLength?: number,
|
||||
showLengthOn?: number,
|
||||
plainText?: true,
|
||||
animate?: boolean,
|
||||
required?: boolean,
|
||||
canBeEdited?: boolean,
|
||||
validate?: () => boolean
|
||||
validate?: () => boolean,
|
||||
inputMode?: 'tel' | 'numeric',
|
||||
withLinebreaks?: boolean,
|
||||
autocomplete?: string
|
||||
};
|
||||
|
||||
class InputField {
|
||||
export default class InputField {
|
||||
public container: HTMLElement;
|
||||
public input: HTMLElement;
|
||||
public inputFake: HTMLElement;
|
||||
public label: HTMLLabelElement;
|
||||
|
||||
public originalValue: string;
|
||||
@ -133,10 +144,6 @@ class InputField {
|
||||
public required: boolean;
|
||||
public validate: () => boolean;
|
||||
|
||||
//public onLengthChange: (length: number, isOverflow: boolean) => void;
|
||||
// protected wasInputFakeClientHeight: number;
|
||||
// protected showScrollDebounced: () => void;
|
||||
|
||||
constructor(public options: InputFieldOptions = {}) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('input-field');
|
||||
@ -148,10 +155,10 @@ class InputField {
|
||||
options.showLengthOn = Math.min(40, Math.round(options.maxLength / 3));
|
||||
}
|
||||
|
||||
const {placeholder, maxLength, showLengthOn, name, plainText, canBeEdited = true} = options;
|
||||
|
||||
let label = options.label || options.labelText;
|
||||
const {placeholder, maxLength, showLengthOn, name, plainText, canBeEdited = true, autocomplete} = options;
|
||||
const label = options.label || options.labelText;
|
||||
|
||||
const onInputCallbacks: Array<() => void> = [];
|
||||
let input: HTMLElement;
|
||||
if(!plainText) {
|
||||
if(init) {
|
||||
@ -163,40 +170,26 @@ class InputField {
|
||||
`;
|
||||
|
||||
input = this.container.firstElementChild as HTMLElement;
|
||||
const observer = new MutationObserver(() => {
|
||||
//checkAndSetRTL(input);
|
||||
// const observer = new MutationObserver(() => {
|
||||
// //checkAndSetRTL(input);
|
||||
|
||||
if(processInput) {
|
||||
processInput();
|
||||
}
|
||||
});
|
||||
// if(processInput) {
|
||||
// processInput();
|
||||
// }
|
||||
// });
|
||||
|
||||
// * because if delete all characters there will br left
|
||||
input.addEventListener('input', () => {
|
||||
onInputCallbacks.push(() => {
|
||||
// * because if delete all characters there will br left
|
||||
if(isInputEmpty(input)) {
|
||||
input.innerHTML = '';
|
||||
}
|
||||
|
||||
if(this.inputFake) {
|
||||
this.inputFake.innerHTML = input.innerHTML;
|
||||
this.onFakeInput();
|
||||
input.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
// ! childList for paste first symbol
|
||||
observer.observe(input, {characterData: true, childList: true, subtree: true});
|
||||
|
||||
if(options.animate) {
|
||||
input.classList.add('scrollable', 'scrollable-y');
|
||||
// this.wasInputFakeClientHeight = 0;
|
||||
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
|
||||
this.inputFake = document.createElement('div');
|
||||
this.inputFake.setAttribute('contenteditable', 'true');
|
||||
this.inputFake.className = input.className + ' input-field-input-fake';
|
||||
}
|
||||
// ! childList for paste first symbol
|
||||
// observer.observe(input, {characterData: true, childList: true, subtree: true});
|
||||
} else {
|
||||
this.container.innerHTML = `
|
||||
<input type="text" ${name ? `name="${name}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
|
||||
<input type="text" ${name ? `name="${name}"` : ''} autocomplete="${autocomplete ?? 'off'}" ${label ? 'required=""' : ''} class="input-field-input">
|
||||
`;
|
||||
|
||||
input = this.container.firstElementChild as HTMLElement;
|
||||
@ -204,13 +197,13 @@ class InputField {
|
||||
}
|
||||
|
||||
input.setAttribute('dir', 'auto');
|
||||
|
||||
if(options.inputMode) {
|
||||
input.inputMode = options.inputMode;
|
||||
}
|
||||
|
||||
if(placeholder) {
|
||||
_i18n(input, placeholder, undefined, 'placeholder');
|
||||
|
||||
if(this.inputFake) {
|
||||
_i18n(this.inputFake, placeholder, undefined, 'placeholder');
|
||||
}
|
||||
}
|
||||
|
||||
if(label || placeholder) {
|
||||
@ -225,12 +218,11 @@ class InputField {
|
||||
this.container.append(this.label);
|
||||
}
|
||||
|
||||
let processInput: () => void;
|
||||
if(maxLength) {
|
||||
const labelEl = this.container.lastElementChild as HTMLLabelElement;
|
||||
let showingLength = false;
|
||||
|
||||
processInput = () => {
|
||||
const onInput = () => {
|
||||
const wasError = input.classList.contains('error');
|
||||
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
|
||||
const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input, false).value].length;
|
||||
@ -250,7 +242,24 @@ class InputField {
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('input', processInput);
|
||||
onInputCallbacks.push(onInput);
|
||||
}
|
||||
|
||||
const noLinebreaks = !options.withLinebreaks;
|
||||
if(noLinebreaks && !plainText) {
|
||||
input.dataset.noLinebreaks = '1';
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if(e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(onInputCallbacks.length) {
|
||||
input.addEventListener('input', () => {
|
||||
onInputCallbacks.forEach((callback) => callback());
|
||||
});
|
||||
}
|
||||
|
||||
this.input = input;
|
||||
@ -277,60 +286,22 @@ class InputField {
|
||||
}
|
||||
}
|
||||
|
||||
public onFakeInput(setHeight = true) {
|
||||
const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake;
|
||||
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
|
||||
this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
|
||||
this.showScrollDebounced();
|
||||
} */
|
||||
|
||||
const currentHeight = +this.input.style.height.replace('px', '');
|
||||
if(currentHeight === newHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TRANSITION_DURATION_FACTOR = 50;
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
|
||||
// this.wasInputFakeClientHeight = clientHeight;
|
||||
this.input.style.transitionDuration = `${transitionDuration}ms`;
|
||||
|
||||
if(setHeight) {
|
||||
this.input.style.height = newHeight ? newHeight + 'px' : '';
|
||||
}
|
||||
|
||||
const className = 'is-changing-height';
|
||||
SetTransition(this.input, className, true, transitionDuration, () => {
|
||||
this.input.classList.remove(className);
|
||||
});
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input, false).value;
|
||||
//return getRichValue(this.input);
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this.setValueSilently(value, false);
|
||||
this.setValueSilently(value, true);
|
||||
|
||||
simulateEvent(this.input, 'input');
|
||||
}
|
||||
|
||||
public setValueSilently(value: string, fireFakeInput = true) {
|
||||
public setValueSilently(value: string, fromSet?: boolean) {
|
||||
if(this.options.plainText) {
|
||||
(this.input as HTMLInputElement).value = value;
|
||||
} else {
|
||||
this.input.innerHTML = value;
|
||||
|
||||
if(this.inputFake) {
|
||||
this.inputFake.innerHTML = value;
|
||||
|
||||
if(fireFakeInput) {
|
||||
this.onFakeInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,7 +319,7 @@ class InputField {
|
||||
return this.isValid() && this.isChanged();
|
||||
}
|
||||
|
||||
public setDraftValue(value = '', silent = false) {
|
||||
public setDraftValue(value = '', silent?: boolean) {
|
||||
if(!this.options.plainText) {
|
||||
value = documentFragmentToHTML(wrapDraftText(value));
|
||||
}
|
||||
@ -360,7 +331,7 @@ class InputField {
|
||||
}
|
||||
}
|
||||
|
||||
public setOriginalValue(value: InputField['originalValue'] = '', silent = false) {
|
||||
public setOriginalValue(value: InputField['originalValue'] = '', silent?: boolean) {
|
||||
this.originalValue = value;
|
||||
this.setDraftValue(value, silent);
|
||||
}
|
||||
@ -369,6 +340,8 @@ class InputField {
|
||||
if(label) {
|
||||
this.label.textContent = '';
|
||||
this.label.append(i18n(label, this.options.labelOptions));
|
||||
} else {
|
||||
this.setLabel();
|
||||
}
|
||||
|
||||
this.input.classList.toggle('error', !!(state & InputState.Error));
|
||||
@ -379,5 +352,3 @@ class InputField {
|
||||
this.setState(InputState.Error, label);
|
||||
}
|
||||
}
|
||||
|
||||
export default InputField;
|
||||
|
76
src/components/inputFieldAnimated.ts
Normal file
76
src/components/inputFieldAnimated.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { _i18n } from "../lib/langPack";
|
||||
import InputField, { InputFieldOptions } from "./inputField";
|
||||
import SetTransition from "./singleTransition";
|
||||
|
||||
export default class InputFieldAnimated extends InputField {
|
||||
public inputFake: HTMLElement;
|
||||
|
||||
//public onLengthChange: (length: number, isOverflow: boolean) => void;
|
||||
// protected wasInputFakeClientHeight: number;
|
||||
// protected showScrollDebounced: () => void;
|
||||
|
||||
constructor(options?: InputFieldOptions) {
|
||||
super(options);
|
||||
|
||||
this.input.addEventListener('input', () => {
|
||||
this.inputFake.innerHTML = this.input.innerHTML;
|
||||
this.onFakeInput();
|
||||
});
|
||||
|
||||
if(options.placeholder) {
|
||||
_i18n(this.inputFake, options.placeholder, undefined, 'placeholder');
|
||||
}
|
||||
|
||||
this.input.classList.add('scrollable', 'scrollable-y');
|
||||
// this.wasInputFakeClientHeight = 0;
|
||||
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
|
||||
this.inputFake = document.createElement('div');
|
||||
this.inputFake.setAttribute('contenteditable', 'true');
|
||||
this.inputFake.className = this.input.className + ' input-field-input-fake';
|
||||
}
|
||||
|
||||
public onFakeInput(setHeight = true) {
|
||||
const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake;
|
||||
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
|
||||
this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
|
||||
this.showScrollDebounced();
|
||||
} */
|
||||
|
||||
const currentHeight = +this.input.style.height.replace('px', '');
|
||||
if(currentHeight === newHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TRANSITION_DURATION_FACTOR = 50;
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
|
||||
// this.wasInputFakeClientHeight = clientHeight;
|
||||
this.input.style.transitionDuration = `${transitionDuration}ms`;
|
||||
|
||||
if(setHeight) {
|
||||
this.input.style.height = newHeight ? newHeight + 'px' : '';
|
||||
}
|
||||
|
||||
const className = 'is-changing-height';
|
||||
SetTransition(this.input, className, true, transitionDuration, () => {
|
||||
this.input.classList.remove(className);
|
||||
});
|
||||
}
|
||||
|
||||
public setValueSilently(value: string, fromSet?: boolean) {
|
||||
super.setValueSilently(value, fromSet);
|
||||
|
||||
this.inputFake.innerHTML = value;
|
||||
if(!fromSet) {
|
||||
this.onFakeInput();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { FontFamily, FontSize, FontWeight } from "../config/font";
|
||||
import getTextWidth from "../helpers/canvas/getTextWidth";
|
||||
import mediaSizes from "../helpers/mediaSizes";
|
||||
import clamp from "../helpers/number/clamp";
|
||||
@ -33,7 +34,6 @@ const map: Map<HTMLElement, {
|
||||
}> = new Map();
|
||||
|
||||
const testQueue: Set<HTMLElement> = new Set();
|
||||
export const fontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
|
||||
const fontSize = '16px';
|
||||
let pendingTest = false;
|
||||
|
||||
@ -90,7 +90,7 @@ function testElement(element: HTMLElement) {
|
||||
multiplier = from > 0 && from / 100;
|
||||
|
||||
//const perf = performance.now();
|
||||
font = `${element.dataset.fontWeight || 400} ${fontSize} ${fontFamily}`;
|
||||
font = `${element.dataset.fontWeight || FontWeight} ${FontSize} ${FontFamily}`;
|
||||
/* const computedStyle = window.getComputedStyle(elm, null);
|
||||
font = `${computedStyle.getPropertyValue('font-weight')} ${computedStyle.getPropertyValue('font-size')} ${computedStyle.getPropertyValue('font-family')}`; */
|
||||
//console.log('testMiddleEllipsis get computed style:', performance.now() - perf, font);
|
||||
|
@ -30,7 +30,7 @@ export default class PopupAvatar extends PopupElement {
|
||||
private onCrop: (upload: () => ReturnType<AppDownloadManager['upload']>) => void;
|
||||
|
||||
constructor() {
|
||||
super('popup-avatar', null, {closable: true, withConfirm: true});
|
||||
super('popup-avatar', {closable: true, withConfirm: true});
|
||||
|
||||
this.h6 = document.createElement('h6');
|
||||
_i18n(this.h6, 'Popup.Avatar.Title');
|
||||
|
@ -15,13 +15,11 @@ import { toastNew } from "../toast";
|
||||
|
||||
export default class PopupCreateContact extends PopupElement {
|
||||
constructor() {
|
||||
super('popup-create-contact popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Add'});
|
||||
super('popup-create-contact popup-send-photo popup-new-media', {closable: true, withConfirm: 'Add', title: 'AddContactTitle'});
|
||||
this.construct();
|
||||
}
|
||||
|
||||
private async construct() {
|
||||
_i18n(this.title, 'AddContactTitle');
|
||||
|
||||
attachClickEvent(this.btnConfirm, () => {
|
||||
const promise = this.managers.appUsersManager.importContact(nameInputField.value, lastNameInputField.value, telInputField.value);
|
||||
|
||||
|
@ -27,7 +27,7 @@ const MAX_LENGTH_SOLUTION = 200;
|
||||
export default class PopupCreatePoll extends PopupElement {
|
||||
private questionInputField: InputField;
|
||||
private questions: HTMLElement;
|
||||
private scrollable: Scrollable;
|
||||
protected scrollable: Scrollable;
|
||||
private tempId = 0;
|
||||
|
||||
private anonymousCheckboxField: CheckboxField;
|
||||
@ -39,13 +39,11 @@ export default class PopupCreatePoll extends PopupElement {
|
||||
private optionInputFields: InputField[];
|
||||
|
||||
constructor(private chat: Chat) {
|
||||
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'Create', body: true});
|
||||
super('popup-create-poll popup-new-media', {closable: true, withConfirm: 'Create', body: true, title: 'NewPoll'});
|
||||
this.construct();
|
||||
}
|
||||
|
||||
private async construct() {
|
||||
_i18n(this.title, 'NewPoll');
|
||||
|
||||
this.questionInputField = new InputField({
|
||||
placeholder: 'AskAQuestion',
|
||||
label: 'AskAQuestion',
|
||||
|
@ -39,17 +39,23 @@ export default class PopupDatePicker extends PopupElement {
|
||||
withTime: true,
|
||||
showOverflowMonths: true
|
||||
}> & PopupOptions = {}) {
|
||||
super('popup-date-picker', options.noButtons ? [] : [{
|
||||
langKey: 'JumpToDate',
|
||||
callback: () => {
|
||||
if(this.onPick) {
|
||||
this.onPick(this.selectedDate.getTime() / 1000 | 0);
|
||||
super('popup-date-picker', {
|
||||
body: true,
|
||||
overlayClosable: true,
|
||||
buttons: options.noButtons ? [] : [{
|
||||
langKey: 'JumpToDate',
|
||||
callback: () => {
|
||||
if(this.onPick) {
|
||||
this.onPick(this.selectedDate.getTime() / 1000 | 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
langKey: 'Cancel',
|
||||
isCancel: true
|
||||
}], {body: true, overlayClosable: true, ...options});
|
||||
}, {
|
||||
langKey: 'Cancel',
|
||||
isCancel: true
|
||||
}],
|
||||
title: true,
|
||||
...options
|
||||
});
|
||||
|
||||
this.minDate = options.minDate || new Date('2013-08-01T00:00:00');
|
||||
|
||||
|
@ -4,11 +4,10 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import rootScope from "../../lib/rootScope";
|
||||
import ripple from "../ripple";
|
||||
import animationIntersector from "../animationIntersector";
|
||||
import appNavigationController, { NavigationItem } from "../appNavigationController";
|
||||
import { i18n, LangPackKey } from "../../lib/langPack";
|
||||
import { i18n, LangPackKey, _i18n } from "../../lib/langPack";
|
||||
import findUpClassName from "../../helpers/dom/findUpClassName";
|
||||
import blurActiveElement from "../../helpers/dom/blurActiveElement";
|
||||
import ListenerSetter from "../../helpers/listenerSetter";
|
||||
@ -20,6 +19,7 @@ import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/f
|
||||
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
|
||||
import { AppManagers } from "../../lib/appManagers/managers";
|
||||
import overlayCounter from "../../helpers/overlayCounter";
|
||||
import Scrollable from "../scrollable";
|
||||
|
||||
export type PopupButton = {
|
||||
text?: string,
|
||||
@ -37,7 +37,10 @@ export type PopupOptions = Partial<{
|
||||
withConfirm: LangPackKey | boolean,
|
||||
body: boolean,
|
||||
confirmShortcutIsSendShortcut: boolean,
|
||||
withoutOverlay: boolean
|
||||
withoutOverlay: boolean,
|
||||
scrollable: boolean,
|
||||
buttons: Array<PopupButton>,
|
||||
title: boolean | LangPackKey
|
||||
}>;
|
||||
|
||||
export interface PopupElementConstructable<T extends PopupElement = any> {
|
||||
@ -79,22 +82,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
|
||||
protected listenerSetter: ListenerSetter;
|
||||
|
||||
protected confirmShortcutIsSendShortcut: boolean;
|
||||
protected btnConfirmOnEnter: HTMLButtonElement;
|
||||
protected btnConfirmOnEnter: HTMLElement;
|
||||
|
||||
protected withoutOverlay: boolean;
|
||||
|
||||
protected managers: AppManagers;
|
||||
|
||||
constructor(className: string, protected buttons?: Array<PopupButton>, options: PopupOptions = {}) {
|
||||
protected scrollable: Scrollable;
|
||||
|
||||
protected buttons: Array<PopupButton>;
|
||||
|
||||
constructor(className: string, options: PopupOptions = {}) {
|
||||
super(false);
|
||||
this.element.classList.add('popup');
|
||||
this.element.className = 'popup' + (className ? ' ' + className : '');
|
||||
this.container.classList.add('popup-container', 'z-depth-1');
|
||||
|
||||
this.header.classList.add('popup-header');
|
||||
this.title.classList.add('popup-title');
|
||||
|
||||
this.header.append(this.title);
|
||||
if(options.title) {
|
||||
this.title.classList.add('popup-title');
|
||||
if(typeof(options.title) === 'string') {
|
||||
_i18n(this.title, options.title);
|
||||
}
|
||||
|
||||
this.header.append(this.title);
|
||||
}
|
||||
|
||||
this.listenerSetter = new ListenerSetter();
|
||||
this.managers = PopupElement.MANAGERS;
|
||||
@ -140,14 +153,25 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
|
||||
this.container.append(this.body);
|
||||
}
|
||||
|
||||
if(options.scrollable) {
|
||||
const scrollable = this.scrollable = new Scrollable(this.body);
|
||||
scrollable.onAdditionalScroll = () => {
|
||||
scrollable.container.classList.toggle('scrolled-top', !scrollable.scrollTop);
|
||||
scrollable.container.classList.toggle('scrolled-bottom', scrollable.isScrolledDown);
|
||||
};
|
||||
|
||||
scrollable.container.classList.add('scrolled-top', 'scrolled-bottom', 'scrollable-y-bordered');
|
||||
|
||||
if(!this.body) {
|
||||
this.container.insertBefore(scrollable.container, this.header.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
let btnConfirmOnEnter = this.btnConfirm;
|
||||
const buttons = this.buttons = options.buttons;
|
||||
if(buttons?.length) {
|
||||
const buttonsDiv = this.buttonsEl = document.createElement('div');
|
||||
buttonsDiv.classList.add('popup-buttons');
|
||||
|
||||
if(buttons.length === 2) {
|
||||
buttonsDiv.classList.add('popup-buttons-row');
|
||||
}
|
||||
|
||||
const buttonsElements = buttons.map((b) => {
|
||||
const button = document.createElement('button');
|
||||
@ -187,6 +211,12 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
|
||||
PopupElement.POPUPS.push(this);
|
||||
}
|
||||
|
||||
protected onContentUpdate() {
|
||||
if(this.scrollable) {
|
||||
this.scrollable.onAdditionalScroll();
|
||||
}
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.navigationItem = {
|
||||
type: 'popup',
|
||||
@ -201,22 +231,32 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
|
||||
void this.element.offsetWidth; // reflow
|
||||
this.element.classList.add('active');
|
||||
|
||||
this.onContentUpdate();
|
||||
|
||||
if(!this.withoutOverlay) {
|
||||
overlayCounter.isOverlayActive = true;
|
||||
animationIntersector.checkAnimations(true);
|
||||
}
|
||||
|
||||
// cannot add event instantly because keydown propagation will fire it
|
||||
if(this.btnConfirmOnEnter) {
|
||||
// if(this.btnConfirmOnEnter) {
|
||||
setTimeout(() => {
|
||||
if(!this.element.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerSetter.add(document.body)('keydown', (e) => {
|
||||
if(PopupElement.POPUPS[PopupElement.POPUPS.length - 1] !== this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') {
|
||||
simulateClickEvent(this.btnConfirmOnEnter);
|
||||
cancelEvent(e);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
public hide = () => {
|
||||
|
@ -24,20 +24,25 @@ export default class PopupJoinChatInvite extends PopupElement {
|
||||
private hash: string,
|
||||
private chatInvite: ChatInvite.chatInvite,
|
||||
) {
|
||||
super('popup-join-chat-invite', addCancelButton([{
|
||||
langKey: chatInvite.pFlags.request_needed ? 'RequestJoin.Button' : (chatInvite.pFlags.broadcast ? 'JoinByPeekChannelTitle' : 'JoinByPeekGroupTitle'),
|
||||
callback: () => {
|
||||
this.managers.appChatsManager.importChatInvite(hash)
|
||||
.then((chatId) => {
|
||||
const peerId = chatId.toPeerId(true);
|
||||
appImManager.setInnerPeer({peerId});
|
||||
}, (error) => {
|
||||
if(error.type === 'INVITE_REQUEST_SENT') {
|
||||
toastNew({langPackKey: 'RequestToJoinSent'});
|
||||
}
|
||||
});
|
||||
}
|
||||
}]), {closable: true, overlayClosable: true, body: true});
|
||||
super('popup-join-chat-invite', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
buttons: addCancelButton([{
|
||||
langKey: chatInvite.pFlags.request_needed ? 'RequestJoin.Button' : (chatInvite.pFlags.broadcast ? 'JoinByPeekChannelTitle' : 'JoinByPeekGroupTitle'),
|
||||
callback: () => {
|
||||
this.managers.appChatsManager.importChatInvite(hash)
|
||||
.then((chatId) => {
|
||||
const peerId = chatId.toPeerId(true);
|
||||
appImManager.setInnerPeer({peerId});
|
||||
}, (error) => {
|
||||
if(error.type === 'INVITE_REQUEST_SENT') {
|
||||
toastNew({langPackKey: 'RequestToJoinSent'});
|
||||
}
|
||||
});
|
||||
}
|
||||
}])
|
||||
});
|
||||
|
||||
this.construct();
|
||||
}
|
||||
|
@ -9,9 +9,29 @@ import { LangPackKey } from "../../lib/langPack";
|
||||
import { MUTE_UNTIL } from "../../lib/mtproto/mtproto_config";
|
||||
import RadioField from "../radioField";
|
||||
import Row, { RadioFormFromRows } from "../row";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
import PopupPeer from "./peer";
|
||||
|
||||
const ONE_HOUR = 3600;
|
||||
const times: {time: number, langKey: LangPackKey}[] = [{
|
||||
time: ONE_HOUR,
|
||||
langKey: 'ChatList.Mute.1Hour'
|
||||
}, {
|
||||
time: ONE_HOUR * 4,
|
||||
langKey: 'ChatList.Mute.4Hours'
|
||||
}, {
|
||||
time: ONE_HOUR * 8,
|
||||
langKey: 'ChatList.Mute.8Hours'
|
||||
}, {
|
||||
time: ONE_HOUR * 24,
|
||||
langKey: 'ChatList.Mute.1Day'
|
||||
}, {
|
||||
time: ONE_HOUR * 24 * 3,
|
||||
langKey: 'ChatList.Mute.3Days'
|
||||
}, {
|
||||
time: -1,
|
||||
langKey: 'ChatList.Mute.Forever'
|
||||
}];
|
||||
|
||||
export default class PopupMute extends PopupPeer {
|
||||
constructor(peerId: PeerId) {
|
||||
super('popup-mute', {
|
||||
@ -26,27 +46,6 @@ export default class PopupMute extends PopupPeer {
|
||||
body: true
|
||||
});
|
||||
|
||||
const ONE_HOUR = 3600;
|
||||
const times: {time: number, langKey: LangPackKey}[] = [{
|
||||
time: ONE_HOUR,
|
||||
langKey: 'ChatList.Mute.1Hour'
|
||||
}, {
|
||||
time: ONE_HOUR * 4,
|
||||
langKey: 'ChatList.Mute.4Hours'
|
||||
}, {
|
||||
time: ONE_HOUR * 8,
|
||||
langKey: 'ChatList.Mute.8Hours'
|
||||
}, {
|
||||
time: ONE_HOUR * 24,
|
||||
langKey: 'ChatList.Mute.1Day'
|
||||
}, {
|
||||
time: ONE_HOUR * 24 * 3,
|
||||
langKey: 'ChatList.Mute.3Days'
|
||||
}, {
|
||||
time: -1,
|
||||
langKey: 'ChatList.Mute.Forever'
|
||||
}];
|
||||
|
||||
const name = 'mute-time';
|
||||
const rows = times.map((time) => {
|
||||
const row = new Row({
|
||||
@ -65,11 +64,9 @@ export default class PopupMute extends PopupPeer {
|
||||
time = +value;
|
||||
});
|
||||
|
||||
rows[rows.length - 1].radioField.checked = true;
|
||||
this.body.append(radioForm);
|
||||
|
||||
const section = new SettingSection({noShadow: true, noDelimiter: true});
|
||||
section.content.append(radioForm);
|
||||
this.body.append(section.container);
|
||||
rows[rows.length - 1].radioField.checked = true;
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export default class PopupNewMedia extends PopupElement {
|
||||
private captionLengthMax: number;
|
||||
|
||||
constructor(private chat: Chat, private files: File[], willAttachType: PopupNewMedia['willAttach']['type']) {
|
||||
super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true, body: true});
|
||||
super('popup-send-photo popup-new-media', {closable: true, withConfirm: 'Modal.Send', confirmShortcutIsSendShortcut: true, body: true, title: true});
|
||||
this.construct(willAttachType);
|
||||
}
|
||||
|
||||
@ -112,7 +112,8 @@ export default class PopupNewMedia extends PopupElement {
|
||||
placeholder: 'PreviewSender.CaptionPlaceholder',
|
||||
label: 'Caption',
|
||||
name: 'photo-caption',
|
||||
maxLength: this.captionLengthMax
|
||||
maxLength: this.captionLengthMax,
|
||||
withLinebreaks: true
|
||||
});
|
||||
this.input = this.inputField.input;
|
||||
|
||||
|
787
src/components/popups/payment.ts
Normal file
787
src/components/popups/payment.ts
Normal file
@ -0,0 +1,787 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import Currencies from "../../config/currencies";
|
||||
import { FontFamily, FontSize } from "../../config/font";
|
||||
import accumulate from "../../helpers/array/accumulate";
|
||||
import getTextWidth from "../../helpers/canvas/getTextWidth";
|
||||
import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
|
||||
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import findUpAsChild from "../../helpers/dom/findUpAsChild";
|
||||
import findUpClassName from "../../helpers/dom/findUpClassName";
|
||||
import loadScript from "../../helpers/dom/loadScript";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||
import replaceContent from "../../helpers/dom/replaceContent";
|
||||
import setInnerHTML from "../../helpers/dom/setInnerHTML";
|
||||
import toggleDisability from "../../helpers/dom/toggleDisability";
|
||||
import { formatPhoneNumber } from "../../helpers/formatPhoneNumber";
|
||||
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
|
||||
import ScrollSaver from "../../helpers/scrollSaver";
|
||||
import tsNow from "../../helpers/tsNow";
|
||||
import { AccountTmpPassword, InputPaymentCredentials, LabeledPrice, Message, MessageMedia, PaymentRequestedInfo, PaymentSavedCredentials, PaymentsPaymentForm, PaymentsPaymentReceipt, PaymentsValidatedRequestedInfo, PostAddress, ShippingOption } from "../../layer";
|
||||
import I18n, { i18n, LangPackKey, _i18n } from "../../lib/langPack";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
|
||||
import rootScope from "../../lib/rootScope";
|
||||
import AvatarElement from "../avatar";
|
||||
import Button from "../button";
|
||||
import PeerTitle from "../peerTitle";
|
||||
import { putPreloader } from "../putPreloader";
|
||||
import Row from "../row";
|
||||
import { toastNew } from "../toast";
|
||||
import { wrapPhoto } from "../wrappers";
|
||||
import wrapPeerTitle from "../wrappers/peerTitle";
|
||||
import PopupPaymentCard, { PaymentCardDetails, PaymentCardDetailsResult } from "./paymentCard";
|
||||
import PopupPaymentCardConfirmation from "./paymentCardConfirmation";
|
||||
import PopupPaymentShipping, { PaymentShippingAddress } from "./paymentShipping";
|
||||
import PopupPaymentShippingMethods from "./paymentShippingMethods";
|
||||
import PopupPaymentVerification from "./paymentVerification";
|
||||
|
||||
const iconPath = 'assets/img/';
|
||||
const icons = [
|
||||
'amex',
|
||||
'card',
|
||||
'diners',
|
||||
'discover',
|
||||
'jcb',
|
||||
'mastercard',
|
||||
'visa',
|
||||
'unionpay',
|
||||
'logo',
|
||||
];
|
||||
|
||||
export function getPaymentBrandIconPath(brand: string) {
|
||||
if(!icons.includes(brand)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `${iconPath}${brand}.svg`;
|
||||
}
|
||||
|
||||
export function PaymentButton(options: {
|
||||
onClick: () => Promise<any> | void,
|
||||
key?: LangPackKey,
|
||||
textEl?: I18n.IntlElement
|
||||
}) {
|
||||
const textEl = options.textEl ?? new I18n.IntlElement({key: options.key ?? 'PaymentInfo.Done'});
|
||||
const key = textEl.key;
|
||||
const payButton = Button('btn-primary btn-color-primary payment-item-pay');
|
||||
payButton.append(textEl.element);
|
||||
attachClickEvent(payButton, async() => {
|
||||
const result = options.onClick();
|
||||
if(!(result instanceof Promise)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const d = putPreloader(payButton);
|
||||
const toggle = toggleDisability([payButton], true);
|
||||
textEl.compareAndUpdate({key: 'PleaseWait'});
|
||||
try {
|
||||
await result;
|
||||
} catch(err) {
|
||||
if(!(err as any).handled) {
|
||||
console.error('payment button error', err);
|
||||
}
|
||||
|
||||
toggle();
|
||||
textEl.compareAndUpdate({key});
|
||||
d.remove();
|
||||
}
|
||||
});
|
||||
return payButton;
|
||||
}
|
||||
|
||||
export type PaymentsCredentialsToken = {type: 'card', token?: string, id?: string};
|
||||
|
||||
export default class PopupPayment extends PopupElement {
|
||||
private currency: string;
|
||||
private tipButtonsMap: Map<number, HTMLElement>;
|
||||
|
||||
constructor(private message: Message.message) {
|
||||
super('popup-payment', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
scrollable: true,
|
||||
title: true
|
||||
});
|
||||
|
||||
this.tipButtonsMap = new Map();
|
||||
this.d();
|
||||
}
|
||||
|
||||
private async d() {
|
||||
this.element.classList.add('is-loading');
|
||||
this.show();
|
||||
|
||||
let confirmed = false;
|
||||
const onConfirmed = () => {
|
||||
if(confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirmed = true;
|
||||
if(popupPaymentVerification) {
|
||||
popupPaymentVerification.hide();
|
||||
}
|
||||
|
||||
this.hide();
|
||||
showSuccessToast();
|
||||
};
|
||||
|
||||
const showSuccessToast = () => {
|
||||
toastNew({
|
||||
langPackKey: 'PaymentInfoHint',
|
||||
langPackArguments: [
|
||||
paymentsWrapCurrencyAmount(getTotalTotal(), currency),
|
||||
wrapEmojiText(mediaInvoice.title)
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
this.listenerSetter.add(rootScope)('payment_sent', ({peerId, mid}) => {
|
||||
if(this.message.peerId === peerId && this.message.mid === mid) {
|
||||
onConfirmed();
|
||||
}
|
||||
});
|
||||
|
||||
const {message} = this;
|
||||
const mediaInvoice = message.media as MessageMedia.messageMediaInvoice;
|
||||
|
||||
_i18n(this.title, mediaInvoice.receipt_msg_id ? 'PaymentReceipt' : 'PaymentCheckout');
|
||||
if(mediaInvoice.pFlags.test) {
|
||||
this.title.append(' (Test)');
|
||||
}
|
||||
|
||||
const className = 'payment-item';
|
||||
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.classList.add(className);
|
||||
|
||||
const detailsClassName = className + '-details';
|
||||
const details = document.createElement('div');
|
||||
details.classList.add(detailsClassName);
|
||||
|
||||
let photoEl: HTMLElement;
|
||||
if(mediaInvoice.photo) {
|
||||
photoEl = document.createElement('div');
|
||||
photoEl.classList.add(detailsClassName + '-photo', 'media-container-cover');
|
||||
wrapPhoto({
|
||||
photo: mediaInvoice.photo,
|
||||
container: photoEl,
|
||||
boxWidth: 100,
|
||||
boxHeight: 100,
|
||||
size: {_: 'photoSizeEmpty', type: ''}
|
||||
});
|
||||
details.append(photoEl);
|
||||
}
|
||||
|
||||
const linesClassName = detailsClassName + '-lines';
|
||||
const lines = document.createElement('div');
|
||||
lines.classList.add(linesClassName);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.classList.add(linesClassName + '-title');
|
||||
|
||||
const description = document.createElement('div');
|
||||
description.classList.add(linesClassName + '-description');
|
||||
|
||||
const botName = document.createElement('div');
|
||||
botName.classList.add(linesClassName + '-bot-name');
|
||||
|
||||
lines.append(title, description, botName);
|
||||
|
||||
setInnerHTML(title, wrapEmojiText(mediaInvoice.title));
|
||||
setInnerHTML(description, wrapEmojiText(mediaInvoice.description));
|
||||
|
||||
const peerTitle = new PeerTitle();
|
||||
botName.append(peerTitle.element);
|
||||
|
||||
details.append(lines);
|
||||
itemEl.append(details);
|
||||
this.scrollable.append(itemEl);
|
||||
|
||||
const preloaderContainer = document.createElement('div');
|
||||
preloaderContainer.classList.add(className + '-preloader-container');
|
||||
const preloader = putPreloader(preloaderContainer, true);
|
||||
this.scrollable.container.append(preloaderContainer);
|
||||
|
||||
let paymentForm: PaymentsPaymentForm | PaymentsPaymentReceipt;
|
||||
const isReceipt = !!mediaInvoice.receipt_msg_id;
|
||||
|
||||
if(isReceipt) paymentForm = await this.managers.appPaymentsManager.getPaymentReceipt(message.peerId, mediaInvoice.receipt_msg_id);
|
||||
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),
|
||||
savedCredentials && this.managers.passwordManager.getState(),
|
||||
wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()})
|
||||
]);
|
||||
|
||||
console.log(paymentForm, lastRequestedInfo);
|
||||
|
||||
await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()});
|
||||
preloaderContainer.remove();
|
||||
this.element.classList.remove('is-loading');
|
||||
|
||||
const wrapAmount = (amount: string | number, skipSymbol?: boolean) => {
|
||||
return paymentsWrapCurrencyAmount(amount, currency, skipSymbol);
|
||||
};
|
||||
|
||||
const {invoice} = paymentForm;
|
||||
const currency = this.currency = invoice.currency;
|
||||
|
||||
const makeLabel = () => {
|
||||
const labelEl = document.createElement('div');
|
||||
labelEl.classList.add(pricesClassName + '-price');
|
||||
|
||||
const left = document.createElement('span');
|
||||
const right = document.createElement('span');
|
||||
labelEl.append(left, right);
|
||||
return {label: labelEl, left, right};
|
||||
};
|
||||
|
||||
const pricesClassName = className + '-prices';
|
||||
const prices = document.createElement('div');
|
||||
prices.classList.add(pricesClassName);
|
||||
const makePricesElements = (prices: LabeledPrice[]) => {
|
||||
return prices.map((price) => {
|
||||
const {amount, label} = price;
|
||||
|
||||
const _label = makeLabel();
|
||||
_label.left.textContent = label;
|
||||
|
||||
const wrappedAmount = wrapAmount(Math.abs(+amount));
|
||||
_label.right.textContent = (amount < 0 ? '-' : '') + wrappedAmount;
|
||||
|
||||
return _label.label;
|
||||
});
|
||||
};
|
||||
|
||||
const pricesElements = makePricesElements(invoice.prices);
|
||||
|
||||
let getTipsAmount = (): number => 0;
|
||||
let shippingAmount = 0;
|
||||
|
||||
const getTotalTotal = () => totalAmount + getTipsAmount() + shippingAmount;
|
||||
const setTotal = () => {
|
||||
const wrapped = wrapAmount(getTotalTotal());
|
||||
totalLabel.right.textContent = wrapped;
|
||||
payI18n.compareAndUpdate({
|
||||
key: 'PaymentCheckoutPay',
|
||||
args: [wrapped]
|
||||
});
|
||||
};
|
||||
|
||||
const payI18n = new I18n.IntlElement();
|
||||
|
||||
const totalLabel = makeLabel();
|
||||
totalLabel.label.classList.add('is-total');
|
||||
_i18n(totalLabel.left, 'PaymentTransactionTotal');
|
||||
const totalAmount = accumulate(invoice.prices.map(({amount}) => +amount), 0);
|
||||
|
||||
const canTip = invoice.max_tip_amount !== undefined;
|
||||
if(canTip) {
|
||||
const tipsClassName = className + '-tips';
|
||||
|
||||
const currencyData = Currencies[currency];
|
||||
|
||||
getTipsAmount = () => +getInputValue().replace(/\D/g, '');
|
||||
|
||||
const getInputValue = () => {
|
||||
// return input.textContent;
|
||||
return input.value;
|
||||
};
|
||||
|
||||
const setInputWidth = () => {
|
||||
const width = getTextWidth(getInputValue(), `500 ${FontSize} ${FontFamily}`);
|
||||
input.style.width = width + 'px';
|
||||
};
|
||||
|
||||
const setInputValue = (amount: string | number) => {
|
||||
amount = Math.min(+amount, +invoice.max_tip_amount);
|
||||
const wrapped = wrapAmount(amount, true);
|
||||
|
||||
input.value = wrapped;
|
||||
// input.textContent = wrapped;
|
||||
if(document.activeElement === input) {
|
||||
placeCaretAtEnd(input);
|
||||
}
|
||||
|
||||
unsetActiveTip();
|
||||
const tipEl = this.tipButtonsMap.get(amount);
|
||||
if(tipEl) {
|
||||
tipEl.classList.add('active');
|
||||
}
|
||||
|
||||
setInputWidth();
|
||||
setTotal();
|
||||
};
|
||||
|
||||
const tipsLabel = makeLabel();
|
||||
_i18n(tipsLabel.left, mediaInvoice.receipt_msg_id ? 'PaymentTip' : 'PaymentTipOptional');
|
||||
const input = document.createElement('input');
|
||||
input.type = 'tel';
|
||||
// const input: HTMLElement = document.createElement('div');
|
||||
// input.contentEditable = 'true';
|
||||
input.classList.add('input-clear', tipsClassName + '-input');
|
||||
tipsLabel.right.append(input);
|
||||
|
||||
tipsLabel.label.style.cursor = 'text';
|
||||
tipsLabel.label.addEventListener('mousedown', (e) => {
|
||||
if(!findUpAsChild(e.target, input)) {
|
||||
placeCaretAtEnd(input);
|
||||
}
|
||||
});
|
||||
|
||||
const haveToIgnoreEvents = input instanceof HTMLInputElement ? 1 : 2;
|
||||
const onSelectionChange = () => {
|
||||
if(ignoreNextSelectionChange) {
|
||||
--ignoreNextSelectionChange;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
ignoreNextSelectionChange = haveToIgnoreEvents;
|
||||
placeCaretAtEnd(input);
|
||||
// }, 0);
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
|
||||
// cancelEvent(e);
|
||||
setTimeout(() => {
|
||||
ignoreNextSelectionChange = haveToIgnoreEvents;
|
||||
placeCaretAtEnd(input);
|
||||
document.addEventListener('selectionchange', onSelectionChange);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const onFocusOut = () => {
|
||||
input.addEventListener('focus', onFocus, {once: true});
|
||||
document.removeEventListener('selectionchange', onSelectionChange);
|
||||
};
|
||||
|
||||
let ignoreNextSelectionChange: number;
|
||||
input.addEventListener('focusout', onFocusOut);
|
||||
onFocusOut();
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
setInputValue(getTipsAmount());
|
||||
});
|
||||
|
||||
let s = [currencyData.symbol, currencyData.space_between ? ' ' : ''];
|
||||
if(!currencyData.symbol_left) s.reverse();
|
||||
tipsLabel.right[currencyData.symbol_left ? 'prepend' : 'append'](s.join(''));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue(tipAmount);
|
||||
});
|
||||
|
||||
setInputValue(0);
|
||||
|
||||
tipsEl.append(...tipButtons);
|
||||
pricesElements.push(tipsEl);
|
||||
} else {
|
||||
setTotal();
|
||||
}
|
||||
|
||||
pricesElements.push(totalLabel.label);
|
||||
|
||||
prices.append(...pricesElements);
|
||||
itemEl.append(prices);
|
||||
|
||||
///
|
||||
|
||||
const setRowIcon = async(row: Row, icon?: string) => {
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('media-photo');
|
||||
await renderImageFromUrlPromise(img, getPaymentBrandIconPath(icon));
|
||||
let container = row.media;
|
||||
if(!container) {
|
||||
container = row.createMedia('small');
|
||||
container.classList.add('media-container-cover');
|
||||
container.append(img);
|
||||
} else {
|
||||
replaceContent(container, img);
|
||||
}
|
||||
};
|
||||
|
||||
const createRow = (options: ConstructorParameters<typeof Row>[0]) => {
|
||||
if(options.titleLangKey) {
|
||||
options.subtitleLangKey = options.titleLangKey;
|
||||
}
|
||||
|
||||
const row = new Row(options);
|
||||
row.container.classList.add(className + '-row');
|
||||
|
||||
if(options.titleLangKey) {
|
||||
row.subtitle.classList.add('hide');
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
const setRowTitle = (row: Row, textContent: string) => {
|
||||
row.title.textContent = textContent;
|
||||
if(!textContent) {
|
||||
const e = I18n.weakMap.get(row.subtitle) as I18n.IntlElement;
|
||||
row.title.append(i18n(e.key));
|
||||
}
|
||||
|
||||
row.subtitle.classList.toggle('hide', !textContent);
|
||||
};
|
||||
|
||||
const setCardSubtitle = (card: PaymentCardDetailsResult) => {
|
||||
let brand: string;
|
||||
let str: string;
|
||||
let icon: string;
|
||||
if('title' in card) {
|
||||
brand = card.title.split(' ').shift();
|
||||
str = card.title;
|
||||
icon = card.icon;
|
||||
} else {
|
||||
brand = detectUnifiedCardBrand(card.cardNumber);
|
||||
str = brand + ' *' + card.cardNumber.split(' ').pop();
|
||||
}
|
||||
|
||||
methodRow.title.classList.remove('tgico', 'tgico-card_outline');
|
||||
setRowIcon(methodRow, icon || brand.toLowerCase());
|
||||
setRowTitle(methodRow, str);
|
||||
};
|
||||
|
||||
const onMethodClick = () => {
|
||||
new PopupPaymentCard(paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => {
|
||||
previousToken = token, previousCardDetails = card;
|
||||
|
||||
setCardSubtitle(card);
|
||||
});
|
||||
};
|
||||
|
||||
let previousCardDetails: PaymentCardDetailsResult, previousToken: PaymentsCredentialsToken;
|
||||
const methodRow = createRow({
|
||||
titleLangKey: 'PaymentCheckoutMethod',
|
||||
clickable: isReceipt ? undefined : onMethodClick,
|
||||
icon: 'card_outline'
|
||||
});
|
||||
|
||||
methodRow.container.classList.add(className + '-method-row');
|
||||
|
||||
if(savedCredentials) {
|
||||
setCardSubtitle(savedCredentials);
|
||||
} else if((paymentForm as PaymentsPaymentReceipt).credentials_title) {
|
||||
setCardSubtitle({title: (paymentForm as PaymentsPaymentReceipt).credentials_title});
|
||||
}
|
||||
|
||||
const providerRow = createRow({
|
||||
title: providerPeerTitle,
|
||||
subtitleLangKey: 'PaymentCheckoutProvider'
|
||||
});
|
||||
|
||||
const providerAvatar = new AvatarElement();
|
||||
providerAvatar.classList.add('avatar-32');
|
||||
providerRow.createMedia('small').append(providerAvatar);
|
||||
/* await */ providerAvatar.updateWithOptions({peerId: paymentForm.provider_id.toPeerId()});
|
||||
|
||||
let shippingAddressRow: Row, shippingNameRow: Row, shippingEmailRow: Row, shippingPhoneRow: Row, shippingMethodRow: Row;
|
||||
let lastShippingOption: ShippingOption, onShippingAddressClick: (focus?: ConstructorParameters<typeof PopupPaymentShipping>[2]) => void, onShippingMethodClick: () => void;
|
||||
const setShippingTitle = invoice.pFlags.shipping_address_requested ? (shippingAddress?: PaymentShippingAddress) => {
|
||||
if(!shippingAddress) {
|
||||
shippingMethodRow.subtitle.classList.add('hide');
|
||||
replaceContent(shippingMethodRow.title, i18n('PaymentShippingAddress'));
|
||||
return;
|
||||
}
|
||||
|
||||
const postAddress = shippingAddress.shipping_address;
|
||||
setRowTitle(shippingAddressRow, [postAddress.city, postAddress.street_line1, postAddress.street_line2].filter(Boolean).join(', '));
|
||||
shippingMethodRow.container.classList.remove('hide');
|
||||
} : undefined;
|
||||
|
||||
const setShippingInfo = (info: PaymentRequestedInfo) => {
|
||||
setShippingTitle && setShippingTitle(info);
|
||||
shippingNameRow && setRowTitle(shippingNameRow, info.name);
|
||||
shippingEmailRow && setRowTitle(shippingEmailRow, info.email);
|
||||
shippingPhoneRow && setRowTitle(shippingPhoneRow, info.phone && ('+' + formatPhoneNumber(info.phone).formatted));
|
||||
};
|
||||
|
||||
if(!isReceipt) {
|
||||
onShippingAddressClick = (focus) => {
|
||||
new PopupPaymentShipping(paymentForm as PaymentsPaymentForm, message, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => {
|
||||
lastRequestedInfo = requestedInfo;
|
||||
savedInfo = (paymentForm as PaymentsPaymentForm).saved_info = shippingAddress;
|
||||
setShippingInfo(shippingAddress);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if(invoice.pFlags.shipping_address_requested) {
|
||||
const setShippingOption = (shippingOption?: ShippingOption) => {
|
||||
const scrollSaver = new ScrollSaver(this.scrollable, undefined, true);
|
||||
scrollSaver.save();
|
||||
if(lastShippingPricesElements) {
|
||||
lastShippingPricesElements.forEach((node) => node.remove());
|
||||
}
|
||||
|
||||
if(!shippingOption) {
|
||||
shippingAmount = 0;
|
||||
|
||||
setTotal();
|
||||
scrollSaver.restore();
|
||||
this.onContentUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
lastShippingOption = shippingOption;
|
||||
setRowTitle(shippingMethodRow, shippingOption.title);
|
||||
|
||||
shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0);
|
||||
lastShippingPricesElements = makePricesElements(shippingOption.prices);
|
||||
let l = totalLabel.label;
|
||||
if(canTip) l = l.previousElementSibling.previousElementSibling as any;
|
||||
lastShippingPricesElements.forEach((element) => l.parentElement.insertBefore(element, l));
|
||||
|
||||
setTotal();
|
||||
scrollSaver.restore();
|
||||
this.onContentUpdate();
|
||||
};
|
||||
|
||||
shippingAddressRow = createRow({
|
||||
icon: 'location',
|
||||
titleLangKey: 'PaymentShippingAddress',
|
||||
clickable: !isReceipt && onShippingAddressClick.bind(null, undefined)
|
||||
});
|
||||
|
||||
let lastShippingPricesElements: HTMLElement[];
|
||||
shippingMethodRow = createRow({
|
||||
icon: 'car',
|
||||
titleLangKey: 'PaymentCheckoutShippingMethod',
|
||||
clickable: !isReceipt && (onShippingMethodClick = () => {
|
||||
new PopupPaymentShippingMethods(paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => {
|
||||
setShippingOption(shippingOption);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
shippingMethodRow.container.classList.add('hide');
|
||||
|
||||
const shippingOption = (paymentForm as PaymentsPaymentReceipt).shipping;
|
||||
if(shippingOption) {
|
||||
setShippingOption(shippingOption);
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.pFlags.name_requested) {
|
||||
shippingNameRow = createRow({
|
||||
icon: 'newprivate',
|
||||
titleLangKey: 'PaymentCheckoutName',
|
||||
clickable: !isReceipt && onShippingAddressClick.bind(null, 'name')
|
||||
});
|
||||
}
|
||||
|
||||
if(invoice.pFlags.email_requested) {
|
||||
shippingEmailRow = createRow({
|
||||
icon: 'mention',
|
||||
titleLangKey: 'PaymentShippingEmailPlaceholder',
|
||||
clickable: !isReceipt && onShippingAddressClick.bind(null, 'email')
|
||||
});
|
||||
}
|
||||
|
||||
if(invoice.pFlags.phone_requested) {
|
||||
shippingPhoneRow = createRow({
|
||||
icon: 'phone',
|
||||
titleLangKey: 'PaymentCheckoutPhoneNumber',
|
||||
clickable: !isReceipt && onShippingAddressClick.bind(null, 'phone')
|
||||
});
|
||||
}
|
||||
|
||||
if(savedInfo) {
|
||||
setShippingInfo(savedInfo);
|
||||
}
|
||||
|
||||
const rows = [
|
||||
methodRow,
|
||||
providerRow,
|
||||
shippingAddressRow,
|
||||
shippingMethodRow,
|
||||
shippingNameRow,
|
||||
shippingEmailRow,
|
||||
shippingPhoneRow,
|
||||
].filter(Boolean);
|
||||
this.scrollable.append(...[
|
||||
document.createElement('hr'),
|
||||
...rows.map((row) => row.container)
|
||||
].filter(Boolean));
|
||||
|
||||
///
|
||||
let popupPaymentVerification: PopupPaymentVerification, lastTmpPasword: AccountTmpPassword;
|
||||
const onClick = () => {
|
||||
const missingInfo = invoice.pFlags.name_requested && !savedInfo?.name ? 'name' : (invoice.pFlags.email_requested && !savedInfo?.email ? 'email' : (invoice.pFlags.phone_requested && !savedInfo?.phone ? 'phone' : undefined));
|
||||
if(invoice.pFlags.shipping_address_requested) {
|
||||
if(!lastRequestedInfo) {
|
||||
onShippingAddressClick();
|
||||
return;
|
||||
} else if(!lastShippingOption) {
|
||||
onShippingMethodClick();
|
||||
return;
|
||||
}
|
||||
} else if(missingInfo) {
|
||||
onShippingAddressClick(missingInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!previousCardDetails && !lastTmpPasword) {
|
||||
if(!savedCredentials) {
|
||||
onMethodClick();
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(passwordState ?? this.managers.passwordManager.getState()).then((_passwordState) => {
|
||||
new PopupPaymentCardConfirmation(savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => {
|
||||
passwordState = undefined;
|
||||
lastTmpPasword = tmpPassword;
|
||||
simulateClickEvent(payButton);
|
||||
|
||||
// * reserve 5 seconds
|
||||
const diff = tmpPassword.valid_until - tsNow(true) - 5;
|
||||
setTimeout(() => {
|
||||
if(lastTmpPasword === tmpPassword) {
|
||||
lastTmpPasword = undefined;
|
||||
}
|
||||
}, diff * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.resolve().then(async() => {
|
||||
const credentials: InputPaymentCredentials = lastTmpPasword ? {
|
||||
_: 'inputPaymentCredentialsSaved',
|
||||
id: savedCredentials.id,
|
||||
tmp_password: lastTmpPasword.tmp_password
|
||||
} : {
|
||||
_: 'inputPaymentCredentials',
|
||||
data: {
|
||||
_: 'dataJSON',
|
||||
data: JSON.stringify(previousToken.token ? previousToken : {type: previousToken.type, id: previousToken.id})
|
||||
},
|
||||
pFlags: {
|
||||
save: previousCardDetails.save || undefined
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const paymentResult = await this.managers.appPaymentsManager.sendPaymentForm(
|
||||
message.peerId,
|
||||
message.mid,
|
||||
(paymentForm as PaymentsPaymentForm).form_id,
|
||||
lastRequestedInfo?.id,
|
||||
lastShippingOption?.id,
|
||||
credentials,
|
||||
getTipsAmount()
|
||||
);
|
||||
|
||||
if(paymentResult._ === 'payments.paymentResult') {
|
||||
onConfirmed();
|
||||
} else {
|
||||
popupPaymentVerification = new PopupPaymentVerification(paymentResult.url);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
popupPaymentVerification.addEventListener('close', () => {
|
||||
if(confirmed) {
|
||||
resolve();
|
||||
} else {
|
||||
const err = new Error('payment not finished');
|
||||
(err as ApiError).handled = true;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
popupPaymentVerification.addEventListener('finish', () => {
|
||||
onConfirmed();
|
||||
});
|
||||
}
|
||||
|
||||
this.hide();
|
||||
} catch(err) {
|
||||
if((err as ApiError).type === 'BOT_PRECHECKOUT_TIMEOUT') {
|
||||
toastNew({langPackKey: 'Error.AnError'});
|
||||
(err as ApiError).handled = true;
|
||||
} else if((err as ApiError).type === 'TMP_PASSWORD_INVALID') {
|
||||
passwordState = lastTmpPasword = undefined;
|
||||
simulateClickEvent(payButton);
|
||||
(err as ApiError).handled = true;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let payButton: HTMLElement;
|
||||
if(isReceipt) {
|
||||
payButton = PaymentButton({
|
||||
onClick: () => this.hide(),
|
||||
key: 'Done'
|
||||
});
|
||||
} else {
|
||||
payButton = PaymentButton({
|
||||
onClick: onClick,
|
||||
textEl: payI18n
|
||||
});
|
||||
}
|
||||
|
||||
this.body.append(this.btnConfirmOnEnter = payButton);
|
||||
|
||||
this.onContentUpdate();
|
||||
}
|
||||
}
|
545
src/components/popups/paymentCard.ts
Normal file
545
src/components/popups/paymentCard.ts
Normal file
@ -0,0 +1,545 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import cardFormattingPatterns from "../../helpers/cards/cardFormattingPatterns";
|
||||
import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
|
||||
import formatInputValueByPattern from "../../helpers/cards/formatInputValueByPattern";
|
||||
import { validateAnyIncomplete, validateCardExpiry, validateCardNumber } from "../../helpers/cards/validateCard";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||
import noop from "../../helpers/noop";
|
||||
import { PaymentsPaymentForm } from "../../layer";
|
||||
import { LangPackKey, _i18n } from "../../lib/langPack";
|
||||
import { TelegramWebviewEvent } from "../../types";
|
||||
import CheckboxField from "../checkboxField";
|
||||
import confirmationPopup from "../confirmationPopup";
|
||||
import CountryInputField from "../countryInputField";
|
||||
import InputField, { InputFieldOptions, InputState } from "../inputField";
|
||||
import Row from "../row";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
import { getPaymentBrandIconPath, PaymentButton, PaymentsCredentialsToken } from "./payment";
|
||||
import { createVerificationIframe } from "./paymentVerification";
|
||||
|
||||
export type PaymentCardDetails = {
|
||||
cardNumber: string;
|
||||
cardholderName: string;
|
||||
expiryFull: string;
|
||||
expiryMonth: string;
|
||||
expiryYear: string;
|
||||
cvc: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
save?: boolean;
|
||||
};
|
||||
|
||||
export type PaymentCardDetailsShort = {
|
||||
title: string,
|
||||
save?: boolean;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export type PaymentCardDetailsResult = PaymentCardDetails | PaymentCardDetailsShort;
|
||||
|
||||
export class InputFieldCorrected extends InputField {
|
||||
private lastKeyDown: string;
|
||||
private lastTransformed: ReturnType<typeof formatInputValueByPattern>;
|
||||
|
||||
constructor(public options: InputFieldOptions & {
|
||||
formatMethod: typeof cardFormattingPatterns['cardNumber'],
|
||||
validateMethod?: typeof validateCardNumber,
|
||||
errorKeys?: {[code: string]: LangPackKey},
|
||||
optional?: boolean,
|
||||
onChange?: (transformed: InputFieldCorrected['lastTransformed']) => void,
|
||||
onKeyDown?: (e: KeyboardEvent) => void
|
||||
}) {
|
||||
super(options);
|
||||
|
||||
// const handleIncomplete = (t?: any) => {
|
||||
// if(
|
||||
// (!lastTransformed.value && t) ||
|
||||
// lastTransformed.meta.autocorrectComplete ||
|
||||
// lastTransformed.meta.error ||
|
||||
// optional
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
// };
|
||||
|
||||
this.input.addEventListener('keydown', this.onKeyDown);
|
||||
this.input.addEventListener('input', this.onInput);
|
||||
this.input.addEventListener('blur', this.onBlur);
|
||||
}
|
||||
|
||||
private onKeyDown = (e: KeyboardEvent) => {
|
||||
this.lastKeyDown = e.key;
|
||||
this.options.onKeyDown?.(e);
|
||||
};
|
||||
|
||||
private onInput = () => {
|
||||
const value = this.value;
|
||||
const deleting = this.lastKeyDown === 'Backspace' && (((this.lastTransformed && this.lastTransformed.value.length) || 0) - value.length) === 1;
|
||||
const result = this.lastTransformed = formatInputValueByPattern({
|
||||
value: value,
|
||||
getPattern: this.options.formatMethod,
|
||||
deleting,
|
||||
input: this.input
|
||||
});
|
||||
|
||||
const transformedValue = result.value;
|
||||
if(transformedValue !== value) {
|
||||
this.setValueSilently(transformedValue);
|
||||
|
||||
if(result.selection) {
|
||||
(this.input as HTMLInputElement).selectionStart = result.selection.selectionStart;
|
||||
(this.input as HTMLInputElement).selectionEnd = result.selection.selectionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
this.validateNew(transformedValue, {ignoreIncomplete: true/* !result.meta.autocorrectComplete */});
|
||||
|
||||
this.options.onChange?.(result);
|
||||
};
|
||||
|
||||
private onBlur = () => {
|
||||
const value = this.lastTransformed?.value;
|
||||
if(value) {
|
||||
this.validateNew(value);
|
||||
}
|
||||
};
|
||||
|
||||
public update() {
|
||||
this.onInput();
|
||||
}
|
||||
|
||||
public validate = () => {
|
||||
return this.validateNew();
|
||||
};
|
||||
|
||||
public validateNew(
|
||||
value = this.lastTransformed?.value ?? '',
|
||||
t: any = {},
|
||||
justReturn?: boolean
|
||||
) {
|
||||
let result: ReturnType<InputFieldCorrected['options']['validateMethod']>;
|
||||
if(this.options.validateMethod) {
|
||||
result = this.options.validateMethod?.(value, t);
|
||||
} else {
|
||||
result = validateAnyIncomplete(this.lastTransformed, value, t);
|
||||
}
|
||||
|
||||
if(result?.code) {
|
||||
const langPackKey: LangPackKey = this.options.errorKeys?.[result.code];
|
||||
!justReturn && this.setState(InputState.Error, langPackKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
!justReturn && this.setState(InputState.Neutral);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleInputFieldsOnChange(inputFields: (CountryInputField | InputField | InputFieldCorrected)[], _onChange: (valid: boolean) => void) {
|
||||
const onChange = () => {
|
||||
const valid = inputFields.every((inputField) => {
|
||||
return 'validateNew' in inputField ? inputField.validateNew(undefined, undefined, true) : inputField.isValid();
|
||||
});
|
||||
|
||||
_onChange(valid);
|
||||
};
|
||||
|
||||
inputFields.forEach((inputField) => {
|
||||
if(inputField instanceof InputFieldCorrected) {
|
||||
const original = inputField.options.onChange;
|
||||
inputField.options.onChange = (...args: any[]) => {
|
||||
// @ts-ignore
|
||||
original?.(...args);
|
||||
onChange();
|
||||
};
|
||||
|
||||
if('update' in inputField) {
|
||||
inputField.update();
|
||||
}
|
||||
} else {
|
||||
inputField.input.addEventListener('input', onChange);
|
||||
}
|
||||
});
|
||||
|
||||
return {validate: onChange};
|
||||
}
|
||||
|
||||
export function createCountryZipFields(country?: boolean, zip?: boolean) {
|
||||
let countryInputField: CountryInputField, postcodeInputField: InputFieldCorrected;
|
||||
if(country || zip) {
|
||||
if(country) countryInputField = new CountryInputField({
|
||||
noPhoneCodes: true,
|
||||
onCountryChange: () => {
|
||||
postcodeInputField?.update();
|
||||
},
|
||||
required: true,
|
||||
autocomplete: 'country',
|
||||
});
|
||||
if(zip) postcodeInputField = new InputFieldCorrected({
|
||||
label: 'PaymentShippingZipPlaceholder',
|
||||
plainText: true,
|
||||
inputMode: 'numeric',
|
||||
autocomplete: 'postal-code',
|
||||
formatMethod: (/* ...args */) => {
|
||||
const {country} = countryInputField.getSelected();
|
||||
const iso2 = country?.iso2;
|
||||
return cardFormattingPatterns.postalCodeFromCountry(iso2 && iso2.toUpperCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {countryInputField, postcodeInputField};
|
||||
}
|
||||
|
||||
export type PaymentsNativeProvider = 'stripe' | 'smartglocal';
|
||||
export type PaymentsNativeParams = {
|
||||
need_country?: boolean,
|
||||
need_zip?: boolean,
|
||||
need_cardholder_name?: boolean,
|
||||
publishable_key?: string, // stripe
|
||||
public_token?: string, // smartglocal
|
||||
gpay_params: string,
|
||||
};
|
||||
const SUPPORTED_NATIVE_PROVIDERS: Set<PaymentsNativeProvider> = new Set(['stripe', 'smartglocal']);
|
||||
|
||||
export default class PopupPaymentCard extends PopupElement<{
|
||||
finish: (obj: {token: any, card: PaymentCardDetailsResult}) => void
|
||||
}> {
|
||||
constructor(private paymentForm: PaymentsPaymentForm, private savedCard?: PaymentCardDetails) {
|
||||
super('popup-payment popup-payment-card', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
scrollable: SUPPORTED_NATIVE_PROVIDERS.has(paymentForm.native_provider as PaymentsNativeProvider),
|
||||
title: 'PaymentCardInfo'
|
||||
});
|
||||
|
||||
if(SUPPORTED_NATIVE_PROVIDERS.has(paymentForm.native_provider as PaymentsNativeProvider)) {
|
||||
this.d();
|
||||
} else {
|
||||
const iframe = createVerificationIframe(paymentForm.url, (event) => {
|
||||
if(event.eventType !== 'payment_form_submit') {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.eventData;
|
||||
|
||||
const cardOut = {title: data.title, save: false} as any as PaymentCardDetails;
|
||||
this.dispatchEvent('finish', {
|
||||
token: data.credentials,
|
||||
card: cardOut
|
||||
});
|
||||
|
||||
this.hide();
|
||||
|
||||
if(paymentForm.pFlags.can_save_credentials) {
|
||||
confirmationPopup({
|
||||
titleLangKey: 'PaymentCardSavePaymentInformation',
|
||||
descriptionLangKey: 'PaymentCardSavePaymentInformationInfoLine1',
|
||||
button: {
|
||||
langKey: 'Save'
|
||||
}
|
||||
}).then(() => {
|
||||
cardOut.save = true;
|
||||
}, noop);
|
||||
}
|
||||
});
|
||||
|
||||
this.body.append(iframe);
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
private d() {
|
||||
const savedCard = this.savedCard;
|
||||
const cardSection = new SettingSection({name: 'PaymentInfo.Card.Title', noDelimiter: true, noShadow: true});
|
||||
|
||||
const nativeParams: PaymentsNativeParams = JSON.parse(this.paymentForm.native_params.data);
|
||||
|
||||
let lastBrand: string, brandIconTempId = 0, lastBrandImg: HTMLImageElement;
|
||||
const setBrandIcon = (brand: string) => {
|
||||
if(lastBrand === brand) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempId = ++brandIconTempId;
|
||||
lastBrand = brand;
|
||||
|
||||
const path = getPaymentBrandIconPath(brand);
|
||||
if(!path) {
|
||||
if(lastBrandImg) {
|
||||
lastBrandImg.remove();
|
||||
lastBrandImg = undefined;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.classList.add('input-field-icon');
|
||||
renderImageFromUrlPromise(img, path, false).then(() => {
|
||||
if(brandIconTempId !== tempId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(lastBrandImg) {
|
||||
lastBrandImg.replaceWith(img);
|
||||
} else {
|
||||
cardInputField.container.append(img);
|
||||
}
|
||||
|
||||
lastBrandImg = img;
|
||||
});
|
||||
};
|
||||
const cardInputField = new InputFieldCorrected({
|
||||
label: 'PaymentCardNumber',
|
||||
plainText: true,
|
||||
inputMode: 'numeric',
|
||||
autocomplete: 'cc-number',
|
||||
formatMethod: cardFormattingPatterns.cardNumber,
|
||||
validateMethod: validateCardNumber,
|
||||
errorKeys: {
|
||||
invalid: 'PaymentCard.Error.Invalid',
|
||||
incomplete: 'PaymentCard.Error.Incomplete'
|
||||
},
|
||||
onChange: (transformed) => {
|
||||
setBrandIcon(detectUnifiedCardBrand(transformed.value));
|
||||
cvcInputField.update(); // format cvc
|
||||
}
|
||||
});
|
||||
|
||||
let nameInputField: InputField;
|
||||
if(nativeParams.need_cardholder_name) nameInputField = new InputField({
|
||||
label: 'Checkout.NewCard.CardholderNamePlaceholder',
|
||||
maxLength: 255,
|
||||
required: true,
|
||||
autocomplete: 'cc-name',
|
||||
});
|
||||
|
||||
const expireInputField = new InputFieldCorrected({
|
||||
label: 'SecureId.Identity.Placeholder.ExpiryDate',
|
||||
plainText: true,
|
||||
inputMode: 'numeric',
|
||||
autocomplete: 'cc-exp',
|
||||
formatMethod: cardFormattingPatterns.cardExpiry,
|
||||
validateMethod: validateCardExpiry
|
||||
});
|
||||
|
||||
const cvcInputField = new InputFieldCorrected({
|
||||
labelText: 'CVC',
|
||||
plainText: true,
|
||||
inputMode: 'numeric',
|
||||
autocomplete: 'cc-csc',
|
||||
formatMethod: () => cardFormattingPatterns.cardCvc(cardInputField.value),
|
||||
// validateMethod: (...args) => _5AH3.a.cardCvc(cardInputField.value)(...args)
|
||||
});
|
||||
|
||||
const switchFocusOrder: (InputFieldCorrected | InputField)[] = [
|
||||
cardInputField,
|
||||
expireInputField,
|
||||
cvcInputField,
|
||||
nameInputField
|
||||
].filter(Boolean);
|
||||
switchFocusOrder.forEach((inputField) => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if(!inputField.value && e.key === 'Backspace') {
|
||||
const previousInputField = switchFocusOrder[switchFocusOrder.indexOf(inputField) - 1];
|
||||
if(previousInputField) {
|
||||
// previousInputField.value = previousInputField.value.slice(0, -1);
|
||||
placeCaretAtEnd(previousInputField.input);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(inputField instanceof InputFieldCorrected) {
|
||||
inputField.options.onKeyDown = onKeyDown;
|
||||
|
||||
const original = inputField.options.onChange;
|
||||
inputField.options.onChange = (transformed) => {
|
||||
original?.(transformed);
|
||||
|
||||
if(document.activeElement === inputField.input && transformed.meta.autocorrectComplete) {
|
||||
for(let i = switchFocusOrder.indexOf(inputField), length = switchFocusOrder.length; i < length; ++i) {
|
||||
const nextInputField = switchFocusOrder[i];
|
||||
if(
|
||||
nextInputField instanceof InputFieldCorrected ?
|
||||
!nextInputField.validateNew(undefined, undefined, true) :
|
||||
!nextInputField.value
|
||||
) {
|
||||
placeCaretAtEnd(nextInputField.input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
inputField.input.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
});
|
||||
|
||||
const inputFieldsRow = document.createElement('div');
|
||||
inputFieldsRow.classList.add('input-fields-row');
|
||||
inputFieldsRow.append(expireInputField.container, cvcInputField.container);
|
||||
|
||||
cardSection.content.append(...[
|
||||
cardInputField.container,
|
||||
inputFieldsRow,
|
||||
nameInputField?.container
|
||||
].filter(Boolean));
|
||||
|
||||
let billingSection: SettingSection;
|
||||
let saveCheckboxField: CheckboxField;
|
||||
const {countryInputField, postcodeInputField} = createCountryZipFields(nativeParams.need_country, nativeParams.need_zip);
|
||||
if(nativeParams.need_country || nativeParams.need_zip) {
|
||||
billingSection = new SettingSection({name: 'PaymentInfo.Billing.Title', noDelimiter: true, noShadow: true});
|
||||
|
||||
// const inputFieldsRow2 = inputFieldsRow.cloneNode() as HTMLElement;
|
||||
// inputFieldsRow2.append(countryInputField.container, postcodeInputField.container);
|
||||
// billingSection.content.append(inputFieldsRow2);
|
||||
billingSection.content.append(...[countryInputField, postcodeInputField].filter(Boolean).map((i) => i.container));
|
||||
}
|
||||
|
||||
const canSave = !!this.paymentForm.pFlags.can_save_credentials;
|
||||
saveCheckboxField = new CheckboxField({
|
||||
text: 'PaymentCardSavePaymentInformation',
|
||||
checked: !!canSave
|
||||
});
|
||||
const saveRow = new Row({
|
||||
checkboxField: saveCheckboxField,
|
||||
subtitleLangKey: canSave ? 'PaymentCardSavePaymentInformationInfoLine1' : 'Checkout.2FA.Text',
|
||||
noCheckboxSubtitle: true
|
||||
});
|
||||
|
||||
if(!canSave) {
|
||||
saveRow.container.classList.add('is-disabled');
|
||||
}
|
||||
|
||||
(billingSection || cardSection).content.append(saveRow.container);
|
||||
|
||||
this.scrollable.append(...[cardSection, billingSection].filter(Boolean).map((s) => s.container));
|
||||
|
||||
const payButton = PaymentButton({
|
||||
key: 'PaymentInfo.Done',
|
||||
onClick: async() => {
|
||||
const data: PaymentCardDetails = {
|
||||
cardNumber: cardInputField.value,
|
||||
expiryFull: expireInputField.value,
|
||||
expiryMonth: expireInputField.value.split('/')[0],
|
||||
expiryYear: expireInputField.value.split('/')[1],
|
||||
cvc: cvcInputField.value,
|
||||
|
||||
cardholderName: nameInputField?.value,
|
||||
country: countryInputField?.value,
|
||||
zip: postcodeInputField?.value,
|
||||
|
||||
save: saveCheckboxField?.checked
|
||||
};
|
||||
|
||||
const nativeProvider: PaymentsNativeProvider = this.paymentForm.native_provider as any;
|
||||
let out: PaymentsCredentialsToken;
|
||||
if(nativeProvider === 'stripe') {
|
||||
const url = new URL('https://api.stripe.com/v1/tokens');
|
||||
url.search = new URLSearchParams({
|
||||
'card[number]': data.cardNumber,
|
||||
'card[exp_month]': data.expiryMonth,
|
||||
'card[exp_year]': data.expiryYear,
|
||||
'card[cvc]': data.cvc,
|
||||
'card[address_zip]': data.zip,
|
||||
'card[address_country]': data.country,
|
||||
'card[name]': data.cardholderName,
|
||||
}).toString();
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${nativeParams.publishable_key}`,
|
||||
},
|
||||
});
|
||||
|
||||
out = await response.json();
|
||||
} else if(nativeProvider === 'smartglocal') {
|
||||
const params = {
|
||||
card: {
|
||||
number: data.cardNumber.replace(/[^\d]+/g, ''),
|
||||
expiration_month: data.expiryMonth,
|
||||
expiration_year: data.expiryYear,
|
||||
security_code: data.cvc.replace(/[^\d]+/g, ''),
|
||||
},
|
||||
};
|
||||
|
||||
const url = /* DEBUG_PAYMENT_SMART_GLOCAL */false
|
||||
? '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': nativeParams.public_token,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
const json: { // smartglocal
|
||||
data: {
|
||||
info: {
|
||||
card_network: string,
|
||||
card_type: string,
|
||||
masked_card_number: string
|
||||
},
|
||||
token: string
|
||||
},
|
||||
status: 'ok'
|
||||
} = await response.json();
|
||||
|
||||
out = {type: 'card', token: json.data.token}
|
||||
}
|
||||
|
||||
this.dispatchEvent('finish', {token: out, card: data});
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
const inputFields = ([
|
||||
cardInputField,
|
||||
nameInputField,
|
||||
expireInputField,
|
||||
cvcInputField,
|
||||
countryInputField,
|
||||
postcodeInputField
|
||||
] as const).filter(Boolean);
|
||||
handleInputFieldsOnChange(inputFields, (valid) => {
|
||||
payButton.disabled = !valid;
|
||||
// payButton.classList.toggle('btn-disabled', !valid);
|
||||
});
|
||||
|
||||
if(savedCard) {
|
||||
cardInputField.value = savedCard.cardNumber;
|
||||
expireInputField.value = savedCard.expiryFull;
|
||||
cvcInputField.value = savedCard.cvc;
|
||||
nameInputField && (nameInputField.value = savedCard.cardholderName);
|
||||
countryInputField && (countryInputField.value = savedCard.country);
|
||||
postcodeInputField && (postcodeInputField.value = savedCard.zip);
|
||||
}
|
||||
|
||||
this.body.append(this.btnConfirmOnEnter = payButton);
|
||||
|
||||
this.show();
|
||||
|
||||
if(!cardInputField.validateNew(undefined, undefined, true)) {
|
||||
placeCaretAtEnd(cardInputField.input);
|
||||
}
|
||||
}
|
||||
}
|
70
src/components/popups/paymentCardConfirmation.ts
Normal file
70
src/components/popups/paymentCardConfirmation.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import { AccountPassword, AccountTmpPassword } from "../../layer";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import { InputState } from "../inputField";
|
||||
import PasswordInputField from "../passwordInputField";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
import { PaymentButton } from "./payment";
|
||||
|
||||
export default class PopupPaymentCardConfirmation extends PopupElement<{
|
||||
finish: (tmpPassword: AccountTmpPassword) => void
|
||||
}> {
|
||||
constructor(card: string, passwordState: AccountPassword) {
|
||||
super('popup-payment popup-payment-card-confirmation', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
scrollable: true,
|
||||
title: 'Checkout.PasswordEntry.Title'
|
||||
});
|
||||
|
||||
const section = new SettingSection({noDelimiter: true, noShadow: true, caption: 'Checkout.PasswordEntry.Text', captionArgs: [card]});
|
||||
const passwordInputField = new PasswordInputField({labelText: passwordState.hint});
|
||||
section.content.append(passwordInputField.container);
|
||||
this.scrollable.append(section.container);
|
||||
|
||||
const onInput = () => {
|
||||
payButton.disabled = !passwordInputField.value;
|
||||
passwordInputField.setState(InputState.Neutral);
|
||||
};
|
||||
|
||||
passwordInputField.input.addEventListener('input', onInput);
|
||||
|
||||
const payButton = PaymentButton({
|
||||
key: 'Checkout.PasswordEntry.Pay',
|
||||
onClick: async() => {
|
||||
try {
|
||||
const inputCheckPassword = await this.managers.passwordManager.getInputCheckPassword(passwordInputField.value, passwordState);
|
||||
const tmpPassword = await this.managers.apiManager.invokeApi('account.getTmpPassword', {
|
||||
password: inputCheckPassword,
|
||||
period: 60
|
||||
});
|
||||
|
||||
this.dispatchEvent('finish', tmpPassword);
|
||||
this.hide();
|
||||
} catch(err) {
|
||||
if((err as ApiError).type === 'PASSWORD_HASH_INVALID') {
|
||||
(err as ApiError).handled = true;
|
||||
passwordInputField.setError('PASSWORD_HASH_INVALID');
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.body.append(this.btnConfirmOnEnter = payButton);
|
||||
|
||||
onInput();
|
||||
|
||||
this.show();
|
||||
|
||||
placeCaretAtEnd(passwordInputField.input);
|
||||
}
|
||||
}
|
231
src/components/popups/paymentShipping.ts
Normal file
231
src/components/popups/paymentShipping.ts
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import { attachClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
|
||||
import toggleDisability from "../../helpers/dom/toggleDisability";
|
||||
import { Message, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
|
||||
import getServerMessageId from "../../lib/appManagers/utils/messageId/getServerMessageId";
|
||||
import { ApiError } from "../../lib/mtproto/apiManager";
|
||||
import matchEmail from "../../lib/richTextProcessor/matchEmail";
|
||||
import Button from "../button";
|
||||
import CheckboxField from "../checkboxField";
|
||||
import CountryInputField from "../countryInputField";
|
||||
import InputField from "../inputField";
|
||||
import Row from "../row";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
import TelInputField from "../telInputField";
|
||||
import { PaymentButton } from "./payment";
|
||||
import { createCountryZipFields, handleInputFieldsOnChange, InputFieldCorrected } from "./paymentCard";
|
||||
|
||||
export type PaymentShippingAddress = PaymentRequestedInfo;
|
||||
|
||||
type ShippingFocusField = 'name' | 'email' | 'phone';
|
||||
|
||||
export default class PopupPaymentShipping extends PopupElement<{
|
||||
finish: (o: {shippingAddress: PaymentShippingAddress, requestedInfo: PaymentsValidatedRequestedInfo}) => void
|
||||
}> {
|
||||
constructor(
|
||||
private paymentForm: PaymentsPaymentForm,
|
||||
private message: Message.message,
|
||||
private focus?: ShippingFocusField
|
||||
) {
|
||||
super('popup-payment popup-payment-shipping', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
scrollable: true,
|
||||
title: 'PaymentShippingInfo'
|
||||
});
|
||||
|
||||
this.d();
|
||||
}
|
||||
|
||||
private d() {
|
||||
const paymentForm = this.paymentForm;
|
||||
const invoice = paymentForm.invoice;
|
||||
const savedInfo = this.paymentForm.saved_info;
|
||||
|
||||
let addressSection: SettingSection,
|
||||
address1InputField: InputField,
|
||||
address2InputField: InputField,
|
||||
cityInputField: InputField,
|
||||
stateInputField: InputField,
|
||||
countryInputField: CountryInputField,
|
||||
postcodeInputField: InputFieldCorrected;
|
||||
if(invoice.pFlags.shipping_address_requested) {
|
||||
addressSection = new SettingSection({name: 'PaymentShippingAddress', noDelimiter: true, noShadow: true});
|
||||
address1InputField = new InputField({label: 'PaymentShippingAddress1Placeholder', maxLength: 64, required: true});
|
||||
address2InputField = new InputField({label: 'PaymentShippingAddress2Placeholder', maxLength: 64});
|
||||
cityInputField = new InputField({label: 'PaymentShippingCityPlaceholder', maxLength: 64, required: true});
|
||||
stateInputField = new InputField({label: 'PaymentShippingStatePlaceholder', maxLength: 64});
|
||||
const res = createCountryZipFields(true, true);
|
||||
countryInputField = res.countryInputField;
|
||||
postcodeInputField = res.postcodeInputField;
|
||||
|
||||
addressSection.content.append(...[
|
||||
address1InputField,
|
||||
address2InputField,
|
||||
cityInputField,
|
||||
stateInputField,
|
||||
countryInputField,
|
||||
postcodeInputField
|
||||
].filter(Boolean).map((inputField) => inputField.container));
|
||||
}
|
||||
|
||||
let receiverSection: SettingSection;
|
||||
let nameInputField: InputField, emailInputField: InputField, telInputField: TelInputField;
|
||||
if([invoice.pFlags.name_requested, invoice.pFlags.email_requested, invoice.pFlags.phone_requested].includes(true)) {
|
||||
receiverSection = new SettingSection({name: 'PaymentShippingReceiver', noDelimiter: true, noShadow: true});
|
||||
|
||||
const validateEmail = () => {
|
||||
const value = emailInputField.value;
|
||||
const match = matchEmail(value);
|
||||
if(!match || match[0].length !== value.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePhone = () => {
|
||||
return !!telInputField.value.match(/\d/);
|
||||
};
|
||||
|
||||
if(invoice.pFlags.name_requested) nameInputField = new InputField({label: 'PaymentShippingName', maxLength: 256, required: true});
|
||||
if(invoice.pFlags.email_requested) emailInputField = new InputField({label: 'PaymentShippingEmailPlaceholder', maxLength: 64, required: true, validate: validateEmail});
|
||||
if(invoice.pFlags.phone_requested) telInputField = new TelInputField({required: true, validate: validatePhone});
|
||||
|
||||
receiverSection.content.append(...[
|
||||
nameInputField,
|
||||
emailInputField,
|
||||
telInputField,
|
||||
].filter(Boolean).map((inputField) => inputField.container));
|
||||
}
|
||||
|
||||
const saveCheckboxField = new CheckboxField({
|
||||
text: 'PaymentShippingSave',
|
||||
checked: true
|
||||
});
|
||||
const saveRow = new Row({
|
||||
checkboxField: saveCheckboxField,
|
||||
subtitleLangKey: 'PaymentShippingSaveInfo',
|
||||
noCheckboxSubtitle: true
|
||||
});
|
||||
|
||||
(receiverSection || addressSection).content.append(saveRow.container);
|
||||
|
||||
this.scrollable.append(...[addressSection, receiverSection].filter(Boolean).map((section) => section.container));
|
||||
|
||||
const payButton = PaymentButton({
|
||||
key: 'PaymentInfo.Done',
|
||||
onClick: async() => {
|
||||
const selectedCountry = countryInputField && countryInputField.getSelected().country;
|
||||
const data: PaymentShippingAddress = {
|
||||
_: 'paymentRequestedInfo',
|
||||
shipping_address: selectedCountry && {
|
||||
_: 'postAddress',
|
||||
street_line1: address1InputField.value,
|
||||
street_line2: address2InputField.value,
|
||||
city: cityInputField.value,
|
||||
state: stateInputField.value,
|
||||
// country: countryInputField.value,
|
||||
country_iso2: selectedCountry?.iso2,
|
||||
post_code: postcodeInputField.value,
|
||||
},
|
||||
name: nameInputField?.value,
|
||||
email: emailInputField?.value,
|
||||
phone: telInputField?.value
|
||||
};
|
||||
|
||||
try {
|
||||
const requestedInfo = await this.managers.appPaymentsManager.validateRequestedInfo(this.message.peerId, this.message.mid, data, saveCheckboxField?.checked);
|
||||
|
||||
this.dispatchEvent('finish', {
|
||||
shippingAddress: data,
|
||||
requestedInfo
|
||||
});
|
||||
|
||||
this.hide();
|
||||
} catch(err: any) {
|
||||
const errorMap: {[err: string]: InputField} = {
|
||||
ADDRESS_STREET_LINE1_INVALID: address1InputField,
|
||||
ADDRESS_STREET_LINE2_INVALID: address2InputField,
|
||||
ADDRESS_COUNTRY_INVALID: countryInputField,
|
||||
ADDRESS_CITY_INVALID: cityInputField,
|
||||
ADDRESS_STATE_INVALID: stateInputField,
|
||||
ADDRESS_POSTCODE_INVALID: postcodeInputField,
|
||||
|
||||
REQ_INFO_NAME_INVALID: nameInputField,
|
||||
REQ_INFO_EMAIL_INVALID: emailInputField,
|
||||
REQ_INFO_PHONE_INVALID: telInputField
|
||||
};
|
||||
|
||||
const inputField = errorMap[(err as ApiError).type];
|
||||
if(inputField) {
|
||||
inputField.setError();
|
||||
(err as any).handled = true;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.body.append(this.btnConfirmOnEnter = payButton);
|
||||
|
||||
if(savedInfo) {
|
||||
const shippingAddress = savedInfo.shipping_address;
|
||||
if(shippingAddress) {
|
||||
address1InputField.value = shippingAddress.street_line1;
|
||||
address2InputField.value = shippingAddress.street_line2;
|
||||
cityInputField.value = shippingAddress.city;
|
||||
stateInputField.value = shippingAddress.state;
|
||||
countryInputField.selectCountryByIso2(shippingAddress.country_iso2);
|
||||
postcodeInputField.value = shippingAddress.post_code;
|
||||
}
|
||||
|
||||
savedInfo.name && nameInputField && (nameInputField.value = savedInfo.name);
|
||||
savedInfo.email && emailInputField && (emailInputField.value = savedInfo.email);
|
||||
savedInfo.phone && telInputField && (telInputField.value = savedInfo.phone);
|
||||
}
|
||||
|
||||
const {validate} = handleInputFieldsOnChange([
|
||||
address1InputField,
|
||||
address2InputField,
|
||||
cityInputField,
|
||||
stateInputField,
|
||||
countryInputField,
|
||||
postcodeInputField,
|
||||
nameInputField,
|
||||
emailInputField,
|
||||
telInputField
|
||||
].filter(Boolean), (valid) => {
|
||||
payButton.disabled = !valid;
|
||||
});
|
||||
|
||||
validate();
|
||||
|
||||
this.show();
|
||||
|
||||
let focusField: InputField;
|
||||
if(this.focus) {
|
||||
const focusMap: {[field in ShippingFocusField]?: InputField} = {
|
||||
name: nameInputField,
|
||||
email: emailInputField,
|
||||
phone: telInputField
|
||||
};
|
||||
|
||||
focusField = focusMap[this.focus];
|
||||
} else {
|
||||
focusField = address1InputField;
|
||||
}
|
||||
|
||||
if(focusField) {
|
||||
placeCaretAtEnd(focusField.input);
|
||||
}
|
||||
}
|
||||
}
|
80
src/components/popups/paymentShippingMethods.ts
Normal file
80
src/components/popups/paymentShippingMethods.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import accumulate from "../../helpers/array/accumulate";
|
||||
import { attachClickEvent } from "../../helpers/dom/clickEvent";
|
||||
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
|
||||
import { PaymentsPaymentForm, PaymentsValidatedRequestedInfo, ShippingOption } from "../../layer";
|
||||
import Button from "../button";
|
||||
import RadioField from "../radioField";
|
||||
import Row, { RadioFormFromRows } from "../row";
|
||||
import { SettingSection } from "../sidebarLeft";
|
||||
import { PaymentButton } from "./payment";
|
||||
|
||||
export default class PopupPaymentShippingMethods extends PopupElement<{
|
||||
finish: (shippingOption: ShippingOption) => void
|
||||
}> {
|
||||
constructor(
|
||||
private paymentForm: PaymentsPaymentForm,
|
||||
private requestedInfo: PaymentsValidatedRequestedInfo,
|
||||
private shippingOption: ShippingOption
|
||||
) {
|
||||
super('popup-payment popup-payment-shipping-methods', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
scrollable: true,
|
||||
title: 'PaymentShippingMethod'
|
||||
});
|
||||
|
||||
this.d();
|
||||
}
|
||||
|
||||
private d() {
|
||||
const section = new SettingSection({name: 'PaymentCheckoutShippingMethod', noDelimiter: true, noShadow: true});
|
||||
|
||||
const rows = this.requestedInfo.shipping_options.map((shippingOption) => {
|
||||
return new Row({
|
||||
radioField: new RadioField({
|
||||
text: shippingOption.title,
|
||||
name: 'shipping-method',
|
||||
value: shippingOption.id
|
||||
}),
|
||||
subtitle: paymentsWrapCurrencyAmount(
|
||||
accumulate(shippingOption.prices.map(({amount}) => +amount), 0),
|
||||
this.paymentForm.invoice.currency
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
let lastShippingId: string;
|
||||
const form = RadioFormFromRows(rows, (value) => {
|
||||
lastShippingId = value;
|
||||
});
|
||||
|
||||
if(this.shippingOption) {
|
||||
rows.find((row) => row.radioField.input.value === this.shippingOption.id).radioField.checked = true;
|
||||
} else {
|
||||
rows[0].radioField.checked = true;
|
||||
}
|
||||
|
||||
section.content.append(form);
|
||||
|
||||
this.scrollable.append(section.container);
|
||||
|
||||
const payButton = PaymentButton({
|
||||
key: 'PaymentInfo.Done',
|
||||
onClick: () => {
|
||||
this.dispatchEvent('finish', this.requestedInfo.shipping_options.find((option) => option.id === lastShippingId));
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
this.body.append(this.btnConfirmOnEnter = payButton);
|
||||
|
||||
this.show();
|
||||
}
|
||||
}
|
64
src/components/popups/paymentVerification.ts
Normal file
64
src/components/popups/paymentVerification.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import PopupElement from ".";
|
||||
import appImManager from "../../lib/appManagers/appImManager";
|
||||
import { TelegramWebviewEventCallback } from "../../types";
|
||||
|
||||
const weakMap: WeakMap<Window, TelegramWebviewEventCallback> = new WeakMap();
|
||||
window.addEventListener('message', (e) => {
|
||||
const callback = weakMap.get(e.source as Window);
|
||||
if(!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(JSON.parse(e.data));
|
||||
});
|
||||
|
||||
export function createVerificationIframe(url: string, callback: TelegramWebviewEventCallback) {
|
||||
const iframe = document.createElement('iframe');
|
||||
// iframe.title = 'Complete Payment';
|
||||
iframe.allow = 'payment';
|
||||
iframe.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin allow-top-navigation allow-modals');
|
||||
iframe.classList.add('payment-verification');
|
||||
iframe.src = url;
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
weakMap.set(iframe.contentWindow, callback);
|
||||
}, {once: true});
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
export default class PopupPaymentVerification extends PopupElement<{
|
||||
finish: () => void
|
||||
}> {
|
||||
constructor(private url: string) {
|
||||
super('popup-payment popup-payment-verification', {
|
||||
closable: true,
|
||||
overlayClosable: true,
|
||||
body: true,
|
||||
title: 'Checkout.WebConfirmation.Title'
|
||||
});
|
||||
|
||||
this.d();
|
||||
}
|
||||
|
||||
private d() {
|
||||
const iframe = createVerificationIframe(this.url, (event) => {
|
||||
if(event.eventType !== 'web_app_open_tg_link') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent('finish');
|
||||
this.hide();
|
||||
appImManager.openUrl('https://t.me' + event.eventData.path_full);
|
||||
});
|
||||
|
||||
this.body.append(iframe);
|
||||
this.show();
|
||||
}
|
||||
}
|
@ -15,23 +15,28 @@ export type PopupPeerButtonCallbackCheckboxes = Set<LangPackKey>;
|
||||
export type PopupPeerButtonCallback = (checkboxes?: PopupPeerButtonCallbackCheckboxes) => void;
|
||||
export type PopupPeerCheckboxOptions = CheckboxFieldOptions & {checkboxField?: CheckboxField};
|
||||
|
||||
export type PopupPeerOptions = PopupOptions & Partial<{
|
||||
export type PopupPeerOptions = Omit<PopupOptions, 'buttons' | 'title'> & Partial<{
|
||||
peerId: PeerId,
|
||||
title: string | HTMLElement,
|
||||
titleLangKey?: LangPackKey,
|
||||
titleLangArgs?: any[],
|
||||
noTitle?: boolean,
|
||||
titleLangKey: LangPackKey,
|
||||
titleLangArgs: any[],
|
||||
noTitle: boolean,
|
||||
description: string | DocumentFragment,
|
||||
descriptionLangKey?: LangPackKey,
|
||||
descriptionLangArgs?: any[],
|
||||
buttons?: Array<PopupPeerButton>,
|
||||
descriptionLangKey: LangPackKey,
|
||||
descriptionLangArgs: any[],
|
||||
buttons: Array<PopupPeerButton>,
|
||||
checkboxes: Array<PopupPeerCheckboxOptions>
|
||||
}>;
|
||||
export default class PopupPeer extends PopupElement {
|
||||
protected description: HTMLParagraphElement;
|
||||
|
||||
constructor(private className: string, options: PopupPeerOptions = {}) {
|
||||
super('popup-peer' + (className ? ' ' + className : ''), options.buttons && addCancelButton(options.buttons), {overlayClosable: true, ...options});
|
||||
super('popup-peer' + (className ? ' ' + className : ''), {
|
||||
overlayClosable: true,
|
||||
...options,
|
||||
title: true,
|
||||
buttons: options.buttons && addCancelButton(options.buttons),
|
||||
});
|
||||
|
||||
if(options.peerId) {
|
||||
const avatarEl = new AvatarElement();
|
||||
@ -65,7 +70,7 @@ export default class PopupPeer extends PopupElement {
|
||||
this.container.classList.add('have-checkbox');
|
||||
|
||||
options.checkboxes.forEach((o) => {
|
||||
o.withRipple = false;
|
||||
o.withRipple = true;
|
||||
const checkboxField = new CheckboxField(o);
|
||||
o.checkboxField = checkboxField;
|
||||
fragment.append(checkboxField.label);
|
||||
|
@ -20,7 +20,7 @@ export default class PopupPickUser extends PopupElement {
|
||||
peerId?: number,
|
||||
selfPresence?: LangPackKey
|
||||
}) {
|
||||
super('popup-forward', null, {closable: true, overlayClosable: true, body: true});
|
||||
super('popup-forward', {closable: true, overlayClosable: true, body: true, title: true});
|
||||
|
||||
this.selector = new AppSelectPeers({
|
||||
appendTo: this.body,
|
||||
|
@ -21,10 +21,7 @@ export default class PopupReactedList extends PopupElement {
|
||||
constructor(
|
||||
private message: Message.message
|
||||
) {
|
||||
super('popup-reacted-list', /* [{
|
||||
langKey: 'Close',
|
||||
isCancel: true
|
||||
}] */null, {closable: true, overlayClosable: true, body: true});
|
||||
super('popup-reacted-list', {closable: true, overlayClosable: true, body: true});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ export default class PopupSchedule extends PopupDatePicker {
|
||||
maxDate: getMaxDate(),
|
||||
withTime: true,
|
||||
showOverflowMonths: true,
|
||||
confirmShortcutIsSendShortcut: true
|
||||
confirmShortcutIsSendShortcut: true,
|
||||
title: true
|
||||
});
|
||||
|
||||
this.element.classList.add('popup-schedule');
|
||||
|
@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import I18n, { i18n } from "../../lib/langPack";
|
||||
import Scrollable from "../scrollable";
|
||||
import PopupPeer from "./peer";
|
||||
|
||||
export default class PopupSponsored extends PopupPeer {
|
||||
@ -23,19 +22,11 @@ export default class PopupSponsored extends PopupPeer {
|
||||
window.open(I18n.format('Chat.Message.Sponsored.Link', true));
|
||||
},
|
||||
isCancel: true
|
||||
}]
|
||||
}],
|
||||
scrollable: true
|
||||
});
|
||||
|
||||
const scrollable = new Scrollable(undefined);
|
||||
scrollable.onAdditionalScroll = () => {
|
||||
scrollable.container.classList.toggle('scrolled-top', !scrollable.scrollTop);
|
||||
scrollable.container.classList.toggle('scrolled-bottom', scrollable.isScrolledDown);
|
||||
};
|
||||
|
||||
this.description.replaceWith(scrollable.container);
|
||||
|
||||
scrollable.container.append(this.description);
|
||||
scrollable.container.classList.add('scrolled-top');
|
||||
this.scrollable.append(this.description);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
@ -6,13 +6,11 @@
|
||||
|
||||
import PopupElement from ".";
|
||||
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
|
||||
import Scrollable from "../scrollable";
|
||||
import { wrapSticker } from "../wrappers";
|
||||
import LazyLoadQueue from "../lazyLoadQueue";
|
||||
import { putPreloader } from "../putPreloader";
|
||||
import animationIntersector from "../animationIntersector";
|
||||
import appImManager from "../../lib/appManagers/appImManager";
|
||||
import { StickerSet } from "../../layer";
|
||||
import mediaSizes from "../../helpers/mediaSizes";
|
||||
import { i18n } from "../../lib/langPack";
|
||||
import Button from "../button";
|
||||
@ -28,17 +26,11 @@ const ANIMATION_GROUP = 'STICKERS-POPUP';
|
||||
export default class PopupStickers extends PopupElement {
|
||||
private stickersFooter: HTMLElement;
|
||||
private stickersDiv: HTMLElement;
|
||||
private h6: HTMLElement;
|
||||
|
||||
private set: StickerSet.stickerSet;
|
||||
|
||||
constructor(private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0]) {
|
||||
super('popup-stickers', null, {closable: true, overlayClosable: true, body: true});
|
||||
super('popup-stickers', {closable: true, overlayClosable: true, body: true, scrollable: true, title: true});
|
||||
|
||||
this.h6 = document.createElement('h6');
|
||||
this.h6.append(i18n('Loading'));
|
||||
|
||||
this.header.append(this.h6);
|
||||
this.title.append(i18n('Loading'));
|
||||
|
||||
this.addEventListener('close', () => {
|
||||
animationIntersector.setOnlyOnePlayableGroup('');
|
||||
@ -62,8 +54,7 @@ export default class PopupStickers extends PopupElement {
|
||||
const btn = Button('btn-primary btn-primary-transparent disable-hover', {noRipple: true, text: 'Loading'});
|
||||
this.stickersFooter.append(btn);
|
||||
|
||||
this.body.append(div);
|
||||
const scrollable = new Scrollable(this.body);
|
||||
this.scrollable.append(div);
|
||||
this.body.append(this.stickersFooter);
|
||||
|
||||
// const editButton = document.createElement('button');
|
||||
@ -87,37 +78,29 @@ export default class PopupStickers extends PopupElement {
|
||||
};
|
||||
|
||||
private loadStickerSet() {
|
||||
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then((set) => {
|
||||
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then(async(set) => {
|
||||
if(!set) {
|
||||
toastNew({langPackKey: 'StickerSet.DontExist'});
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
//console.log('PopupStickers loadStickerSet got set:', set);
|
||||
|
||||
this.set = set.set;
|
||||
|
||||
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
|
||||
|
||||
setInnerHTML(this.h6, wrapEmojiText(set.set.title));
|
||||
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
|
||||
|
||||
let button: HTMLElement;
|
||||
const s = i18n('Stickers', [set.set.count]);
|
||||
if(set.set.installed_date) {
|
||||
button = Button('btn-primary btn-primary-transparent danger', {noRipple: true});
|
||||
button.append(i18n('RemoveStickersCount', [i18n('Stickers', [set.set.count])]));
|
||||
button.append(i18n('RemoveStickersCount', [s]));
|
||||
} else {
|
||||
button = Button('btn-primary btn-color-primary', {noRipple: true});
|
||||
button.append(i18n('AddStickersCount', [i18n('Stickers', [set.set.count])]));
|
||||
button.append(i18n('AddStickersCount', [s]));
|
||||
}
|
||||
|
||||
this.stickersFooter.textContent = '';
|
||||
this.stickersFooter.append(button);
|
||||
|
||||
attachClickEvent(button, () => {
|
||||
const toggle = toggleDisability([button], true);
|
||||
|
||||
this.managers.appStickersManager.toggleStickerSet(this.set).then(() => {
|
||||
this.managers.appStickersManager.toggleStickerSet(set.set).then(() => {
|
||||
this.hide();
|
||||
}).catch(() => {
|
||||
toggle();
|
||||
@ -125,12 +108,9 @@ export default class PopupStickers extends PopupElement {
|
||||
});
|
||||
|
||||
const lazyLoadQueue = new LazyLoadQueue();
|
||||
|
||||
this.stickersDiv.classList.remove('is-loading');
|
||||
this.stickersDiv.innerHTML = '';
|
||||
for(let doc of set.documents) {
|
||||
const divs = await Promise.all(set.documents.map(async(doc) => {
|
||||
if(doc._ === 'documentEmpty') {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
@ -138,7 +118,7 @@ export default class PopupStickers extends PopupElement {
|
||||
|
||||
const size = mediaSizes.active.esgSticker.width;
|
||||
|
||||
wrapSticker({
|
||||
await wrapSticker({
|
||||
doc,
|
||||
div,
|
||||
lazyLoadQueue,
|
||||
@ -149,8 +129,19 @@ export default class PopupStickers extends PopupElement {
|
||||
height: size
|
||||
});
|
||||
|
||||
this.stickersDiv.append(div);
|
||||
}
|
||||
return div;
|
||||
}));
|
||||
|
||||
setInnerHTML(this.title, wrapEmojiText(set.set.title));
|
||||
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
|
||||
this.stickersFooter.textContent = '';
|
||||
this.stickersFooter.append(button);
|
||||
|
||||
this.stickersDiv.classList.remove('is-loading');
|
||||
this.stickersDiv.innerHTML = '';
|
||||
this.stickersDiv.append(...divs.filter(Boolean));
|
||||
|
||||
this.scrollable.onAdditionalScroll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -664,6 +664,8 @@ export type SettingSectionOptions = {
|
||||
name?: LangPackKey,
|
||||
nameArgs?: FormatterArguments,
|
||||
caption?: LangPackKey | true,
|
||||
captionArgs?: FormatterArguments,
|
||||
captionOld?: SettingSectionOptions['caption'],
|
||||
noDelimiter?: boolean,
|
||||
fakeGradientDelimiter?: boolean,
|
||||
noShadow?: boolean,
|
||||
@ -721,13 +723,17 @@ export class SettingSection {
|
||||
|
||||
container.append(innerContainer);
|
||||
|
||||
if(options.caption) {
|
||||
const caption = this.caption = this.generateContentElement();
|
||||
caption.classList.add(className + '-caption');
|
||||
container.append(caption);
|
||||
const caption = options.caption ?? options.captionOld;
|
||||
if(caption) {
|
||||
const el = this.caption = this.generateContentElement();
|
||||
el.classList.add(className + '-caption');
|
||||
|
||||
if(options.caption !== true) {
|
||||
i18n_({element: caption, key: options.caption});
|
||||
if(!options.captionOld) {
|
||||
container.append(el);
|
||||
}
|
||||
|
||||
if(caption !== true) {
|
||||
i18n_({element: el, key: caption, args: options.captionArgs});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export default class AppTwoStepVerificationEmailTab extends SliderSuperTab {
|
||||
this.setTitle('RecoveryEmailTitle');
|
||||
|
||||
const section = new SettingSection({
|
||||
caption: true,
|
||||
captionOld: true,
|
||||
noDelimiter: true
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,7 @@ export default class AppTwoStepVerificationEmailConfirmationTab extends SliderSu
|
||||
this.setTitle('TwoStepAuth.RecoveryTitle');
|
||||
|
||||
const section = new SettingSection({
|
||||
caption: true,
|
||||
captionOld: true,
|
||||
noDelimiter: true
|
||||
});
|
||||
|
||||
|
@ -130,7 +130,7 @@ export default class AppTwoStepVerificationEnterPasswordTab extends SliderSuperT
|
||||
switch(err.type) {
|
||||
default:
|
||||
//btnContinue.innerText = err.type;
|
||||
textEl.key = 'TwoStepAuth.InvalidPassword';
|
||||
textEl.key = 'PASSWORD_HASH_INVALID';
|
||||
textEl.update();
|
||||
preloader.remove();
|
||||
passwordInputField.select();
|
||||
|
@ -25,7 +25,7 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab {
|
||||
this.setTitle('TwoStepVerificationTitle');
|
||||
|
||||
const section = new SettingSection({
|
||||
caption: true,
|
||||
captionOld: true,
|
||||
noDelimiter: true
|
||||
});
|
||||
|
||||
|
@ -17,7 +17,7 @@ export default class AppTwoStepVerificationSetTab extends SliderSuperTab {
|
||||
this.setTitle('TwoStepVerificationPasswordSet');
|
||||
|
||||
const section = new SettingSection({
|
||||
caption: 'TwoStepVerificationPasswordSetInfo',
|
||||
captionOld: 'TwoStepVerificationPasswordSetInfo',
|
||||
noDelimiter: true
|
||||
});
|
||||
|
||||
|
@ -29,6 +29,9 @@ import toggleDisability from "../../../helpers/dom/toggleDisability";
|
||||
import convertKeyToInputKey from "../../../helpers/string/convertKeyToInputKey";
|
||||
import getPrivacyRulesDetails from "../../../lib/appManagers/utils/privacy/getPrivacyRulesDetails";
|
||||
import PrivacyType from "../../../lib/appManagers/utils/privacy/privacyType";
|
||||
import confirmationPopup, { PopupConfirmationOptions } from "../../confirmationPopup";
|
||||
import noop from "../../../helpers/noop";
|
||||
import { toastNew } from "../../toast";
|
||||
|
||||
export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
|
||||
private activeSessionsRow: Row;
|
||||
@ -315,6 +318,48 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
|
||||
this.scrollable.append(section.container);
|
||||
}
|
||||
|
||||
{
|
||||
const section = new SettingSection({name: 'PrivacyPayments', caption: 'PrivacyPaymentsClearInfo'});
|
||||
|
||||
const onClearClick = () => {
|
||||
const options: PopupConfirmationOptions = {
|
||||
titleLangKey: 'PrivacyPaymentsClearAlertTitle',
|
||||
descriptionLangKey: 'PrivacyPaymentsClearAlertText',
|
||||
button: {
|
||||
langKey: 'Clear'
|
||||
},
|
||||
checkboxes: [{
|
||||
text: 'PrivacyClearShipping',
|
||||
checked: true
|
||||
}, {
|
||||
text: 'PrivacyClearPayment',
|
||||
checked: true
|
||||
}]
|
||||
};
|
||||
|
||||
confirmationPopup(options).then(() => {
|
||||
const [info, payment] = options.checkboxes.map((c) => c.checkboxField.checked);
|
||||
const toggle = toggleDisability([clearButton], true);
|
||||
this.managers.appPaymentsManager.clearSavedInfo(info, payment).then(() => {
|
||||
if(!info && !payment) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggle();
|
||||
toastNew({
|
||||
langPackKey: info && payment ? 'PrivacyPaymentsPaymentShippingCleared' : (info ? 'PrivacyPaymentsShippingInfoCleared' : 'PrivacyPaymentsPaymentInfoCleared')
|
||||
});
|
||||
});
|
||||
}, noop);
|
||||
};
|
||||
|
||||
const clearButton = Button('btn-primary btn-transparent', {icon: 'delete', text: 'PrivacyPaymentsClear'});
|
||||
this.listenerSetter.add(clearButton)('click', onClearClick);
|
||||
section.content.append(clearButton);
|
||||
|
||||
this.scrollable.append(section.container);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export class UsernameInputField extends InputField {
|
||||
|
||||
//console.log('userNameInput:', value);
|
||||
if(value === this.originalValue || !value.length) {
|
||||
this.setState(InputState.Neutral, this.options.label);
|
||||
this.setState(InputState.Neutral);
|
||||
this.options.onChange && this.options.onChange();
|
||||
return;
|
||||
} else if(!isUsernameValid(value)) { // does not check the last underscore
|
||||
|
@ -12,6 +12,7 @@ import formatCallDuration from "../../helpers/formatCallDuration";
|
||||
import paymentsWrapCurrencyAmount from "../../helpers/paymentsWrapCurrencyAmount";
|
||||
import { Message, MessageAction } from "../../layer";
|
||||
import { MyMessage } from "../../lib/appManagers/appMessagesManager";
|
||||
import getPeerId from "../../lib/appManagers/utils/peers/getPeerId";
|
||||
import I18n, { FormatterArgument, FormatterArguments, i18n, join, langPack, LangPackKey, _i18n } from "../../lib/langPack";
|
||||
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
|
||||
import wrapPlainText from "../../lib/richTextProcessor/wrapPlainText";
|
||||
@ -254,7 +255,11 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage,
|
||||
args = [price, getNameDivHTML(message.peerId, plain)];
|
||||
|
||||
if(message.reply_to_mid) {
|
||||
const invoiceMessage = await managers.appMessagesManager.getMessageByPeer(message.peerId, message.reply_to_mid);
|
||||
const invoiceMessage = await managers.appMessagesManager.getMessageByPeer(
|
||||
message.reply_to?.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : message.peerId,
|
||||
message.reply_to_mid
|
||||
);
|
||||
|
||||
if(!invoiceMessage) {
|
||||
managers.appMessagesManager.fetchMessageReplyTo(message);
|
||||
} else {
|
||||
|
@ -19,7 +19,7 @@ const App = {
|
||||
version: process.env.VERSION,
|
||||
versionFull: process.env.VERSION_FULL,
|
||||
build: +process.env.BUILD,
|
||||
langPackVersion: '0.4.1',
|
||||
langPackVersion: '0.4.4',
|
||||
langPack: 'macos',
|
||||
langPackCode: 'en',
|
||||
domains: [MAIN_DOMAIN] as string[],
|
||||
|
9
src/config/font.ts
Normal file
9
src/config/font.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
export const FontFamily = 'Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
|
||||
export const FontSize = '16px';
|
||||
export const FontWeight = '400';
|
5
src/helpers/array/createArray.ts
Normal file
5
src/helpers/array/createArray.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default function createArray<T1>(length: number, fill?: T1, map?: any): T1[] {
|
||||
const arr = new Array<T1>(length);
|
||||
arr.fill(fill);
|
||||
return map ? arr.map(map) : arr;
|
||||
}
|
9
src/helpers/cacheCallback.ts
Normal file
9
src/helpers/cacheCallback.ts
Normal file
@ -0,0 +1,9 @@
|
||||
function cacheCallback<A, T>(callback: (str: A) => T) {
|
||||
const stringResults: any = {}, numberResults: any = {};
|
||||
return (value: A): T => {
|
||||
const key = '_' + value;
|
||||
return (typeof(value) === 'string' ? stringResults : numberResults)[key] ??= callback(value);
|
||||
};
|
||||
}
|
||||
|
||||
export default cacheCallback;
|
112
src/helpers/cards/cardBrands.ts
Normal file
112
src/helpers/cards/cardBrands.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import cacheCallback from "../cacheCallback";
|
||||
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)/,
|
||||
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)/
|
||||
};
|
||||
|
||||
// * taken from Stripe
|
||||
export const CARD_BRANDS: {[b: string]: {
|
||||
minLength: number,
|
||||
maxLength: number,
|
||||
cvcMaxLength: number,
|
||||
cvcMinLength: number | null
|
||||
}} = {
|
||||
visa: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
mastercard: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
amex: {
|
||||
minLength: 15,
|
||||
maxLength: 15,
|
||||
cvcMaxLength: 4,
|
||||
cvcMinLength: 3
|
||||
},
|
||||
unionpay: {
|
||||
minLength: 13,
|
||||
maxLength: 19,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
diners: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
diners14: {
|
||||
minLength: 14,
|
||||
maxLength: 14,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
discover: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
jcb: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
elo: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 3,
|
||||
cvcMinLength: null
|
||||
},
|
||||
unknown: {
|
||||
minLength: 16,
|
||||
maxLength: 16,
|
||||
cvcMaxLength: 4,
|
||||
cvcMinLength: 3
|
||||
}
|
||||
};
|
||||
|
||||
export const detectCardBrand = cacheCallback((card: string = '') => {
|
||||
const keys = Object.keys(CARD_BRAND_REGEXP);
|
||||
const sanitizedCard = replaceNonNumber(card);
|
||||
let brand: string;
|
||||
let last = 0;
|
||||
keys.forEach((key) => {
|
||||
const regExp = CARD_BRAND_REGEXP[key];
|
||||
const match = sanitizedCard.match(regExp);
|
||||
if(match) {
|
||||
const result = match[0];
|
||||
if(result && result.length > last) {
|
||||
brand = key;
|
||||
last = result.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return brand || 'unknown';
|
||||
});
|
||||
|
||||
export function cardBrandToUnifiedBrand(brand: string) {
|
||||
return brand === 'diners14' ? 'diners' : brand;
|
||||
}
|
||||
|
||||
export function detectUnifiedCardBrand(card = '') {
|
||||
const brand = detectCardBrand(card);
|
||||
return cardBrandToUnifiedBrand(brand);
|
||||
}
|
78
src/helpers/cards/cardFormattingPatterns.ts
Normal file
78
src/helpers/cards/cardFormattingPatterns.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { IS_ANDROID } from "../../environment/userAgent";
|
||||
import createArray from "../array/createArray";
|
||||
import cacheCallback from "../cacheCallback";
|
||||
import replaceNonNumber from "../string/replaceNonNumber";
|
||||
import { CARD_BRANDS, detectCardBrand } from "./cardBrands";
|
||||
import patternCharacters from "./patternCharacters";
|
||||
|
||||
const digit = patternCharacters.digit;
|
||||
const capitalCharacter = patternCharacters.capitalCharacter;
|
||||
const spaceCharacter = patternCharacters.formattingCharacter(' ');
|
||||
const yearOptionalPattern = patternCharacters.optionalPattern(/\d\d/);
|
||||
const sixteenPattern = [digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit];
|
||||
const fifteenPattern = [digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit, digit, digit, spaceCharacter, digit, digit, digit, digit];
|
||||
|
||||
const requiredPostcodes = new Set(['DZ', 'AR', 'AM', 'AU', 'AT', 'AZ', 'PT', 'BD', 'BY', 'BE', 'BA', 'BR', 'BN', 'BG', 'CA', 'IC', 'CN', 'CO', 'HR', 'CY', 'CZ', 'DK', 'EC', 'GB', 'EE', 'FO', 'FI', 'FR', 'GE', 'DE', 'GR', 'GL', 'GU', 'GG', 'NL', 'HU', 'IN', 'ID', 'IL', 'IT', 'JP', 'JE', 'KZ', 'KR', 'FM', 'KG', 'LV', 'LI', 'LT', 'LU', 'MK', 'MG', 'PT', 'MY', 'MH', 'MQ', 'YT', 'MX', 'MN', 'ME', 'NL', 'NZ', 'GB', 'NO', 'PK', 'PH', 'PL', 'FM', 'PT', 'PR', 'RE', 'RU', 'SA', 'SF', 'RS', 'SG', 'SK', 'SI', 'ZA', 'ES', 'LK', 'SX', 'VI', 'VI', 'SE', 'CH', 'TW', 'TJ', 'TH', 'TU', 'TN', 'TR', 'TM', 'VI', 'UA', 'GB', 'US', 'UY', 'UZ', 'VA', 'VN', 'GB', 'FM']);
|
||||
|
||||
const generateFourPattern = cacheCallback((length: number) => {
|
||||
const out: Array<typeof digit | typeof spaceCharacter> = [];
|
||||
|
||||
for(let i = 0, k = 0; i < length;) {
|
||||
if(k === 4) {
|
||||
out.push(spaceCharacter);
|
||||
k = 0;
|
||||
} else {
|
||||
out.push(digit);
|
||||
++i;
|
||||
++k;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
function generateCardNumberPattern(card: string) {
|
||||
const brand = detectCardBrand(card);
|
||||
if(brand === 'amex') return sixteenPattern;
|
||||
if(brand === 'diners14') return fifteenPattern;
|
||||
const {minLength, maxLength} = CARD_BRANDS[brand];
|
||||
const s = replaceNonNumber(card).length;
|
||||
const d = Math.min(Math.max(minLength, s), maxLength);
|
||||
return generateFourPattern(d);
|
||||
}
|
||||
|
||||
const cardFormattingPatterns = {
|
||||
cardNumber: generateCardNumberPattern,
|
||||
cardExpiry: () => [patternCharacters.month, patternCharacters.formattingCharacter('/'), digit, digit, yearOptionalPattern],
|
||||
cardCvc: (card?: string) => cardFormattingPatterns.cardCvcFromBrand(detectCardBrand(card)),
|
||||
cardCvcFromBrand: cacheCallback((brand: string) => {
|
||||
const info = CARD_BRANDS[brand];
|
||||
const {cvcMinLength, cvcMaxLength} = info;
|
||||
const pattern = createArray(cvcMinLength || cvcMaxLength, digit);
|
||||
if(cvcMinLength && cvcMinLength < cvcMaxLength) {
|
||||
const i = cvcMaxLength - cvcMinLength;
|
||||
const h = patternCharacters.optionalPattern(/\d/);
|
||||
if(i) {
|
||||
pattern.push(...createArray(i, h));
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}),
|
||||
postalCodeFromCountry: cacheCallback((iso2: string) => {
|
||||
switch(iso2) {
|
||||
case 'US':
|
||||
return createArray(5, digit);
|
||||
case 'CA':
|
||||
return IS_ANDROID ? null : [capitalCharacter, capitalCharacter, capitalCharacter, spaceCharacter, capitalCharacter, capitalCharacter, capitalCharacter];
|
||||
default:
|
||||
const optionalDigits = createArray(10, patternCharacters.optionalPattern(/\d/));
|
||||
if(requiredPostcodes.has(iso2)) {
|
||||
optionalDigits[0] = digit;
|
||||
}
|
||||
return optionalDigits;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export default cardFormattingPatterns;
|
25
src/helpers/cards/formatInputValueByPattern.ts
Normal file
25
src/helpers/cards/formatInputValueByPattern.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import formatValueByPattern from "./formatValueByPattern";
|
||||
|
||||
export default function formatInputValueByPattern(options: {
|
||||
value: string,
|
||||
getPattern: Parameters<typeof formatValueByPattern>[0],
|
||||
deleting?: boolean,
|
||||
input?: HTMLElement
|
||||
}) {
|
||||
const {value: originalValue, getPattern, deleting, input} = options;
|
||||
const pushRest = !deleting && !!originalValue.length;
|
||||
const result = formatValueByPattern(getPattern, originalValue, {
|
||||
selectionStart: input ? (input as HTMLInputElement).selectionStart : 0,
|
||||
selectionEnd: input ? (input as HTMLInputElement).selectionEnd : 0
|
||||
}, pushRest)
|
||||
const {value, selection} = result;
|
||||
|
||||
return {
|
||||
value,
|
||||
meta: {
|
||||
autocorrectComplete: result.autocorrectComplete,
|
||||
empty: !value
|
||||
},
|
||||
selection
|
||||
};
|
||||
}
|
102
src/helpers/cards/formatValueByPattern.ts
Normal file
102
src/helpers/cards/formatValueByPattern.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import accumulate from "../array/accumulate";
|
||||
import { PatternFunction } from "./patternCharacters";
|
||||
|
||||
function accumulateLengths(strs: string[]) {
|
||||
return accumulate(strs.map((str) => str.length), 0);
|
||||
}
|
||||
|
||||
function formatValueByPattern(
|
||||
getPattern: PatternFunction,
|
||||
value: string,
|
||||
options: Partial<{
|
||||
selectionStart: number,
|
||||
selectionEnd: number
|
||||
}> = {},
|
||||
pushRest?: boolean
|
||||
) {
|
||||
const pattern = getPattern(value);
|
||||
|
||||
if(!pattern) {
|
||||
return {
|
||||
value: value,
|
||||
selection: null as typeof options,
|
||||
autocorrectComplete: !!value
|
||||
};
|
||||
}
|
||||
|
||||
const length = pattern.length;
|
||||
const c: string[] = [];
|
||||
const s: string[] = [];
|
||||
|
||||
let l = 0;
|
||||
let i = 0;
|
||||
let f = options.selectionStart === 0 ? 0 : null;
|
||||
let d = options.selectionEnd === 0 ? 0 : null;
|
||||
const p = () => {
|
||||
if(f === null && (i + 1) >= options.selectionStart) f = accumulateLengths(c) + (pushRest ? s.length : 0);
|
||||
if(d === null && (i + 1) >= options.selectionEnd) d = accumulateLengths(c) + (pushRest ? s.length : 0);
|
||||
};
|
||||
const m = (e: number) => {
|
||||
if(e > 0) {
|
||||
p();
|
||||
i += e;
|
||||
}
|
||||
};
|
||||
|
||||
for(; l < length;) {
|
||||
const getCharacter = pattern[l];
|
||||
const character = getCharacter(value.slice(i));
|
||||
const {type, result, consumed} = character;
|
||||
if(type === 'required') {
|
||||
if(result) {
|
||||
c.push(...s, result);
|
||||
s.length = 0;
|
||||
++l;
|
||||
|
||||
if(character.partial) {
|
||||
m(value.length - i);
|
||||
break;
|
||||
}
|
||||
|
||||
m(consumed);
|
||||
} else {
|
||||
if(!consumed) {
|
||||
break;
|
||||
}
|
||||
|
||||
m(1);
|
||||
}
|
||||
} else if(type === 'optional') {
|
||||
if(result) {
|
||||
c.push(...s, result);
|
||||
s.length = 0;
|
||||
m(consumed);
|
||||
}
|
||||
|
||||
++l;
|
||||
} else if(type === 'formatting') {
|
||||
if(!pushRest && i >= value.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
s.push(result);
|
||||
++l;
|
||||
m(consumed);
|
||||
}
|
||||
}
|
||||
|
||||
if(pushRest) {
|
||||
c.push(...s);
|
||||
}
|
||||
|
||||
return {
|
||||
value: c.join(''),
|
||||
selection: {
|
||||
selectionStart: f === null || value.length && options.selectionStart === value.length ? accumulateLengths(c) : f,
|
||||
selectionEnd: d === null || value.length && options.selectionEnd === value.length ? accumulateLengths(c) : d
|
||||
},
|
||||
autocorrectComplete: l === length
|
||||
};
|
||||
}
|
||||
|
||||
export default formatValueByPattern;
|
85
src/helpers/cards/patternCharacters.ts
Normal file
85
src/helpers/cards/patternCharacters.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { fixBuggedNumbers } from "../string/buggedNumbers";
|
||||
import replaceNonNumber from "../string/replaceNonNumber";
|
||||
|
||||
export type PatternCharacter = {
|
||||
type: 'optional',
|
||||
result: string,
|
||||
consumed: number
|
||||
} | {
|
||||
type: 'required',
|
||||
result: string,
|
||||
consumed: number,
|
||||
partial?: boolean
|
||||
} | {
|
||||
type: 'formatting',
|
||||
result: string,
|
||||
consumed: number
|
||||
};
|
||||
|
||||
export type PatternFunction = (str: string) => ((str: string) => PatternCharacter)[];
|
||||
|
||||
function makeOptionalCharacter(result: string, consumed: number): PatternCharacter {
|
||||
return {type: 'optional', result, consumed};
|
||||
}
|
||||
|
||||
function makeRequiredCharacter(result: string, consumed: number, partial?: boolean): PatternCharacter {
|
||||
return {type: 'required', result, consumed, partial};
|
||||
}
|
||||
|
||||
function makeFormattingCharacter(result: string, consumed: number): PatternCharacter {
|
||||
return {type: 'formatting', result, consumed};
|
||||
}
|
||||
|
||||
function wrapCharacterRegExpFactory(regExp: RegExp, optional?: boolean) {
|
||||
return (str: string) => {
|
||||
const _regExp = new RegExp('^'.concat(regExp.source.replace(/^\^/, '')));
|
||||
const match = str.match(_regExp);
|
||||
const makeCharacter = optional ? makeOptionalCharacter : makeRequiredCharacter;
|
||||
if(match) {
|
||||
const result = match[0];
|
||||
return makeCharacter(result, match.index + result.length);
|
||||
}
|
||||
|
||||
return makeCharacter('', str.length);
|
||||
};
|
||||
}
|
||||
|
||||
function makeCapitalPatternCharacter(str: string) {
|
||||
const char = wrapCharacterRegExpFactory(/\w/)(str);
|
||||
return char.result ? makeRequiredCharacter(char.result.toUpperCase(), char.consumed) : char;
|
||||
}
|
||||
|
||||
const makeMonthDigitPatternCharacter = wrapCharacterRegExpFactory(/1[0-2]|0?[1-9]|0/);
|
||||
|
||||
function digit(str: string) {
|
||||
return wrapCharacterRegExpFactory(/[0-9]/)(fixBuggedNumbers(str));
|
||||
}
|
||||
|
||||
const patternCharacters = {
|
||||
digit,
|
||||
capitalCharacter: makeCapitalPatternCharacter,
|
||||
month: (str: string) => {
|
||||
const char = makeMonthDigitPatternCharacter(fixBuggedNumbers(str));
|
||||
const cleanedResult = replaceNonNumber(char.result);
|
||||
const isPartial = ['0', '1'].includes(char.result) && str.length === 1;
|
||||
if(isPartial || (char.result === '0' && str.length >= 2)) {
|
||||
return makeRequiredCharacter(char.result, str.length, true);
|
||||
}
|
||||
|
||||
return makeRequiredCharacter(cleanedResult.length === 1 ? '0' + cleanedResult : cleanedResult, char.consumed);
|
||||
},
|
||||
formattingCharacter: (str: string) => {
|
||||
return (str1: string) => {
|
||||
const consumed = str === str1[0] ? 1 : 0;
|
||||
return makeFormattingCharacter(str, consumed);
|
||||
}
|
||||
},
|
||||
optionalPattern: (regExp: RegExp) => {
|
||||
return (str: string) => {
|
||||
const char = wrapCharacterRegExpFactory(regExp, true)(str);
|
||||
return char.result ? char : makeOptionalCharacter('', 0);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default patternCharacters;
|
82
src/helpers/cards/validateCard.ts
Normal file
82
src/helpers/cards/validateCard.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { CARD_BRANDS, detectCardBrand } from "./cardBrands";
|
||||
import formatInputValueByPattern from './formatInputValueByPattern';
|
||||
import NBSP from "../string/nbsp";
|
||||
import replaceNonNumber from "../string/replaceNonNumber";
|
||||
|
||||
export type PatternValidationOptions = Partial<{
|
||||
ignoreIncomplete: boolean
|
||||
}>;
|
||||
|
||||
const nbspRegExp = new RegExp(NBSP, 'g');
|
||||
|
||||
function makeValidationError(code?: string) {
|
||||
return code ? {
|
||||
type: 'invalid',
|
||||
code
|
||||
} : null;
|
||||
}
|
||||
|
||||
function validateCompleteCardNumber(card: string) {
|
||||
const t = '0'.charCodeAt(0);
|
||||
const n = card.length % 2;
|
||||
let a = 0;
|
||||
for(let i = card.length - 1; i >= 0; --i) {
|
||||
const c = n === (i % 2);
|
||||
let o = card.charCodeAt(i) - t;
|
||||
if(c) o *= 2;
|
||||
if(o > 9) o -= 9;
|
||||
a += o;
|
||||
}
|
||||
return !(a % 10);
|
||||
}
|
||||
|
||||
function validateExpiry(year: number, month: number, options?: PatternValidationOptions) {
|
||||
const date = new Date(Date.now());
|
||||
const _year = year < 100 ? date.getFullYear() % 100 : date.getFullYear();
|
||||
const nextMonth = date.getMonth() + 1;
|
||||
|
||||
if(isNaN(year) || isNaN(month)) {
|
||||
return options?.ignoreIncomplete ? null : 'incomplete';
|
||||
}
|
||||
|
||||
if((year - _year) < 0) {
|
||||
return 'invalid_expiry_year_past';
|
||||
}
|
||||
|
||||
if((year - _year) > 50) {
|
||||
return 'invalid_expiry_year';
|
||||
}
|
||||
|
||||
return !(year - _year) && month < nextMonth ? 'invalid_expiry_month_past' : null;
|
||||
}
|
||||
|
||||
function getCardInfoByNumber(card: string) {
|
||||
const sanitized = replaceNonNumber(card);
|
||||
const brand = detectCardBrand(card);
|
||||
return {
|
||||
sanitized,
|
||||
brand,
|
||||
minLength: CARD_BRANDS[brand].minLength
|
||||
};
|
||||
}
|
||||
|
||||
function makeCardNumberError(str: string, length: number, ignoreIncomplete: boolean) {
|
||||
return str.length >= length ? (validateCompleteCardNumber(str) ? null : makeValidationError('invalid')) : (ignoreIncomplete ? null : makeValidationError('incomplete'));
|
||||
}
|
||||
|
||||
export function validateCardNumber(str: string, options: PatternValidationOptions = {}) {
|
||||
const {sanitized, minLength} = getCardInfoByNumber(str);
|
||||
return makeCardNumberError(sanitized, minLength, options.ignoreIncomplete);
|
||||
}
|
||||
|
||||
export function validateCardExpiry(str: string, options: PatternValidationOptions = {}) {
|
||||
const sanitized = str.replace(nbspRegExp, '').split(/ ?\/ ?/);
|
||||
const [monthStr, yearStr = ''] = sanitized;
|
||||
const [month, year] = [monthStr, yearStr].map((str) => +str);
|
||||
const s = yearStr.length === 2 ? year % 100 : year;
|
||||
return yearStr.length < 2 || yearStr.length === 3 ? (options.ignoreIncomplete ? null : makeValidationError('incomplete')) : makeValidationError(validateExpiry(s, month, options));
|
||||
}
|
||||
|
||||
export function validateAnyIncomplete(formatted: ReturnType<typeof formatInputValueByPattern>, str: string, options: PatternValidationOptions = {}) {
|
||||
return formatted.meta.autocorrectComplete || options.ignoreIncomplete ? null : makeValidationError('incomplete');
|
||||
}
|
17
src/helpers/dom/loadScript.ts
Normal file
17
src/helpers/dom/loadScript.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
export default function loadScript(url: string) {
|
||||
const script = document.createElement('script');
|
||||
const promise = new Promise<HTMLScriptElement>((resolve) => {
|
||||
script.onload = script.onerror = () => {
|
||||
resolve(script);
|
||||
};
|
||||
});
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
return promise;
|
||||
}
|
@ -17,7 +17,11 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals
|
||||
}
|
||||
|
||||
el.focus();
|
||||
if(typeof window.getSelection !== "undefined" && typeof document.createRange !== "undefined") {
|
||||
if(el instanceof HTMLInputElement) {
|
||||
const length = el.value.length;
|
||||
el.selectionStart = length;
|
||||
el.selectionEnd = length;
|
||||
} else if(typeof window.getSelection !== "undefined" && typeof document.createRange !== "undefined") {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
range.collapse(false);
|
||||
@ -33,3 +37,5 @@ export default function placeCaretAtEnd(el: HTMLElement, ignoreTouchCheck = fals
|
||||
textRange.select();
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).placeCaretAtEnd = placeCaretAtEnd;
|
||||
|
@ -1,7 +0,0 @@
|
||||
import bigInt from "big-integer";
|
||||
import intToUint from "../number/intToUint";
|
||||
|
||||
export default function longFromInts(high: number, low: number): string {
|
||||
high = intToUint(high), low = intToUint(low);
|
||||
return bigInt(high).shiftLeft(32).add(bigInt(low)).toString(10);
|
||||
}
|
7
src/helpers/long/ulongFromInts.ts
Normal file
7
src/helpers/long/ulongFromInts.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import bigInt from "big-integer";
|
||||
import intToUint from "../number/intToUint";
|
||||
|
||||
export default function ulongFromInts(high: number, low: number) {
|
||||
high = intToUint(high), low = intToUint(low);
|
||||
return bigInt(high).shiftLeft(32).add(bigInt(low));
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import Currencies from "../config/currencies";
|
||||
|
||||
// https://stackoverflow.com/a/34141813
|
||||
function number_format(number: any, decimals: any, dec_point: any, thousands_sep: any) {
|
||||
function number_format(number: any, decimals: any, dec_point: any, thousands_sep: any): string {
|
||||
// Strip all characters but numerical ones.
|
||||
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
|
||||
var n = !isFinite(+number) ? 0 : +number,
|
||||
@ -25,7 +25,7 @@ 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) {
|
||||
export default function paymentsWrapCurrencyAmount($amount: number | string, $currency: string, $skipSymbol?: boolean) {
|
||||
$amount = +$amount;
|
||||
|
||||
const $currency_data = Currencies[$currency]; // вытащить из json
|
||||
@ -42,8 +42,11 @@ export default function paymentsWrapCurrencyAmount($amount: number | string, $cu
|
||||
}
|
||||
|
||||
const $formatted = number_format($amount_exp, $decimals, $currency_data['decimal_sep'], $currency_data['thousands_sep']);
|
||||
if($skipSymbol) {
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
const $splitter = $currency_data['space_between'] ? "\xc2\xa0" : '';
|
||||
const $splitter = $currency_data['space_between'] ? " " : '';
|
||||
let $formatted_intern: string;
|
||||
if($currency_data['symbol_left']) {
|
||||
$formatted_intern = $currency_data['symbol'] + $splitter + $formatted;
|
||||
|
@ -43,6 +43,8 @@ export default class ScrollSaver {
|
||||
}
|
||||
|
||||
public findElements() {
|
||||
if(!this.query) return [];
|
||||
|
||||
const {container} = this;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const bubbles = Array.from(container.querySelectorAll(this.query)) as HTMLElement[];
|
||||
|
14
src/helpers/string/buggedNumbers.ts
Normal file
14
src/helpers/string/buggedNumbers.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const delta = '0'.charCodeAt(0) - '0'.charCodeAt(0);
|
||||
const buggedRegExp = /[0-9]/g;
|
||||
|
||||
// function hasBuggedNumbers(str: string) {
|
||||
// return !!str.match(a);
|
||||
// }
|
||||
|
||||
function getDistanceFromBuggedToNormal(char: string) {
|
||||
return String.fromCharCode(char.charCodeAt(0) - delta);
|
||||
}
|
||||
|
||||
export function fixBuggedNumbers(str: string) {
|
||||
return str.replace(buggedRegExp, getDistanceFromBuggedToNormal);
|
||||
}
|
2
src/helpers/string/nbsp.ts
Normal file
2
src/helpers/string/nbsp.ts
Normal file
@ -0,0 +1,2 @@
|
||||
const NBSP = '';
|
||||
export default NBSP;
|
3
src/helpers/string/replaceNonLatin.ts
Normal file
3
src/helpers/string/replaceNonLatin.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default function replaceNonLatin(str: string) {
|
||||
return str.replace(/[^A-Za-z0-9]/g, "");
|
||||
}
|
3
src/helpers/string/replaceNonNumber.ts
Normal file
3
src/helpers/string/replaceNonNumber.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default function replaceNonNumber(str: string) {
|
||||
return str.replace(/\D/g, '');
|
||||
}
|
58
src/lang.ts
58
src/lang.ts
@ -111,7 +111,6 @@ const lang = {
|
||||
"Popup.Unpin.HideTitle": "Hide pinned messages",
|
||||
"Popup.Unpin.HideDescription": "Do you want to hide the pinned message bar? It wil stay hidden until a new message is pinned.",
|
||||
"Popup.Unpin.Hide": "Hide",
|
||||
"TwoStepAuth.InvalidPassword": "Invalid password",
|
||||
"TwoStepAuth.EmailCodeChangeEmail": "Change Email",
|
||||
"MarkupTooltip.LinkPlaceholder": "Enter URL...",
|
||||
"MediaViewer.Context.Download": "Download",
|
||||
@ -137,6 +136,12 @@ const lang = {
|
||||
//"PushNotification.Action.Mute1d.Success": "Notification settings were successfully saved.",
|
||||
//it is from iOS
|
||||
"VoiceChat.DiscussionGroup": "discussion group",
|
||||
"PaymentInfo.CVV": "CVV Code",
|
||||
"PaymentInfo.Card.Title": "Enter your card information",
|
||||
"PaymentInfo.Billing.Title": "Enter your billing address",
|
||||
"PaymentInfo.Done": "PROCEED TO CHECKOUT",
|
||||
"PaymentCard.Error.Invalid": "Invalid card number",
|
||||
"PaymentCard.Error.Incomplete": "Incomplete card number",
|
||||
|
||||
// * android
|
||||
"AccDescrEditing": "Editing",
|
||||
@ -702,6 +707,49 @@ const lang = {
|
||||
"PaymentSuccessfullyPaidNoItem": "You successfully transferred %1$s to %2$s",
|
||||
// "PaymentSuccessfullyPaidRecurrent": "You successfully transferred %1$s to %2$s for %3$s and allowed future recurring payments",
|
||||
// "PaymentSuccessfullyPaidNoItemRecurrent": "You successfully transferred %1$s to %2$s and allowed future recurring payments",
|
||||
"PaymentCheckout": "Checkout",
|
||||
"PaymentTransactionTotal": "Total",
|
||||
"PaymentTip": "Tip",
|
||||
"PaymentTipOptional": "Tip (Optional)",
|
||||
"PaymentCheckoutPay": "PAY %1$s",
|
||||
"PaymentCheckoutMethod": "Payment method",
|
||||
"PaymentCheckoutProvider": "Payment provider",
|
||||
"PaymentCardNumber": "Card Number",
|
||||
"PaymentCardSavePaymentInformation": "Save Payment Information",
|
||||
"PaymentCardInfo": "Payment info",
|
||||
"PaymentCardSavePaymentInformationInfoLine1": "You can save your payment info for future use. It will be stored directly with the payment provider. Telegram has no access to your credit card data.",
|
||||
"Done": "Done",
|
||||
"PaymentShippingMethod": "Shipping methods",
|
||||
"PaymentNoShippingMethod": "Sorry, it is not possible to deliver to your address.",
|
||||
"PaymentShippingInfo": "Shipping Information",
|
||||
"PaymentShippingAddress": "Shipping address",
|
||||
"PaymentShippingAddress1Placeholder": "Address 1 (Street)",
|
||||
"PaymentShippingAddress2Placeholder": "Address 2 (Street)",
|
||||
"PaymentShippingCityPlaceholder": "City",
|
||||
"PaymentShippingStatePlaceholder": "State",
|
||||
"PaymentShippingCountry": "Country",
|
||||
"PaymentShippingZipPlaceholder": "Postcode",
|
||||
"PaymentShippingReceiver": "Receiver",
|
||||
"PaymentShippingName": "Full Name",
|
||||
"PaymentShippingEmailPlaceholder": "Email",
|
||||
"PaymentCheckoutPhoneNumber": "Phone number",
|
||||
"PaymentCheckoutShippingMethod": "Shipping method",
|
||||
"PaymentShippingSave": "Save Shipping Information",
|
||||
"PaymentShippingSaveInfo": "You can save your shipping info for future use.",
|
||||
"PaymentInfoHint": "You paid **%1$s** for **%2$s**.",
|
||||
"PrivacyPayments": "Payments",
|
||||
"PrivacyPaymentsClearInfo": "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data.",
|
||||
"PrivacyPaymentsClear": "Clear Payment and Shipping Info",
|
||||
"PrivacyPaymentsClearAlertTitle": "Clear payment info",
|
||||
"PrivacyPaymentsClearAlertText": "Are you sure you want to clear your payment and shipping info?",
|
||||
"PrivacyPaymentsPaymentInfoCleared": "Payment info cleared.",
|
||||
"PrivacyPaymentsShippingInfoCleared": "Shipping info cleared.",
|
||||
"PrivacyPaymentsPaymentShippingCleared": "Payment and shipping info cleared.",
|
||||
"PrivacyClearShipping": "Shipping info",
|
||||
"PrivacyClearPayment": "Payment info",
|
||||
"Clear": "Clear",
|
||||
"Save": "Save",
|
||||
"PaymentCheckoutName": "Name",
|
||||
|
||||
// * macos
|
||||
"AccountSettings.Filters": "Chat Folders",
|
||||
@ -848,6 +896,12 @@ const lang = {
|
||||
"Chat.Message.ViewGroup": "VIEW GROUP",
|
||||
"Chat.Message.Sponsored.What": "What are sponsored messages?",
|
||||
"Chat.Message.Sponsored.Link": "https://promote.telegram.org",
|
||||
"Checkout.2FA.Text": "Saving payment details is only available with 2-Step Verification.",
|
||||
"Checkout.NewCard.CardholderNamePlaceholder": "Cardholder Name",
|
||||
"Checkout.PasswordEntry.Title": "Payment Confirmation",
|
||||
"Checkout.PasswordEntry.Pay": "Pay",
|
||||
"Checkout.PasswordEntry.Text": "Your card %@ is on file. To pay with this card, please enter your 2-Step-Verification password.",
|
||||
"Checkout.WebConfirmation.Title": "Complete Payment",
|
||||
"ChatList.Context.Mute": "Mute",
|
||||
"ChatList.Context.Unmute": "Unmute",
|
||||
"ChatList.Context.Pin": "Pin",
|
||||
@ -906,6 +960,7 @@ const lang = {
|
||||
"Emoji.Objects": "Objects",
|
||||
//"Emoji.Symbols": "Symbols",
|
||||
"Emoji.Flags": "Flags",
|
||||
"Error.AnError": "An error occurred. Please try again later.",
|
||||
"FileSize.B": "%@ B",
|
||||
"FileSize.KB": "%@ KB",
|
||||
"FileSize.MB": "%@ MB",
|
||||
@ -1052,6 +1107,7 @@ const lang = {
|
||||
"GeneralSettings.EmojiPrediction": "Suggest Emoji",
|
||||
"GroupPermission.Delete": "Delete Exception",
|
||||
"Search.Confirm.ClearHistory": "Are you sure you want to clear your search history?",
|
||||
"SecureId.Identity.Placeholder.ExpiryDate": "Expiry Date",
|
||||
"Separator.ShowMore": "show more",
|
||||
"Separator.ShowLess": "show less",
|
||||
"ScheduleController.at": "at",
|
||||
|
@ -1,6 +1,5 @@
|
||||
const lang = {
|
||||
"Login.Title": "Sign in to Telegram",
|
||||
"Login.CountrySelectorLabel": "Country",
|
||||
"Login.PhoneLabel": "Phone Number",
|
||||
"Login.PhoneLabelInvalid": "Phone Number Invalid",
|
||||
"Login.KeepSigned": "Keep me signed in",
|
||||
@ -21,6 +20,7 @@ const lang = {
|
||||
"FirstName": "First name (required)",
|
||||
"LastName": "Last name (optional)",
|
||||
"StartMessaging": "Start Messaging",
|
||||
"Country": "Country",
|
||||
|
||||
// * macos
|
||||
"Contacts.PhoneNumber.Placeholder": "Phone Number",
|
||||
|
@ -294,7 +294,7 @@ export class AppImManager extends EventListenerBase<{
|
||||
|
||||
const onInstanceDeactivated = (reason: InstanceDeactivateReason) => {
|
||||
const isUpdated = reason === 'version';
|
||||
const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true});
|
||||
const popup = new PopupElement('popup-instance-deactivated', {overlayClosable: true});
|
||||
const c = document.createElement('div');
|
||||
c.classList.add('instance-deactivated-container');
|
||||
(popup as any).container.replaceWith(c);
|
||||
|
@ -12,13 +12,14 @@
|
||||
import type { MyDocument } from "./appDocsManager";
|
||||
import type { MyPhoto } from "./appPhotosManager";
|
||||
import type { MyTopPeer } from "./appUsersManager";
|
||||
import { BotInlineResult, GeoPoint, InputGeoPoint, InputMedia, MessageEntity, MessagesBotResults, ReplyMarkup } from "../../layer";
|
||||
import { BotInlineResult, GeoPoint, InputGeoPoint, InputMedia, MessageEntity, MessageMedia, MessagesBotResults, ReplyMarkup } from "../../layer";
|
||||
import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray";
|
||||
import { AppManager } from "./manager";
|
||||
import getPhotoMediaInput from "./utils/photos/getPhotoMediaInput";
|
||||
import getServerMessageId from "./utils/messageId/getServerMessageId";
|
||||
import generateQId from "./utils/inlineBots/generateQId";
|
||||
import getDocumentMediaInput from "./utils/docs/getDocumentMediaInput";
|
||||
import { AppMessagesManager } from "./appMessagesManager";
|
||||
|
||||
export class AppInlineBotsManager extends AppManager {
|
||||
private inlineResults: {[queryAndResultIds: string]: BotInlineResult} = {};
|
||||
@ -286,7 +287,7 @@ export class AppInlineBotsManager extends AppManager {
|
||||
this.appMessagesManager.sendText(peerId, inlineResult.send_message.message, options);
|
||||
} else {
|
||||
let caption = '';
|
||||
let inputMedia: InputMedia;
|
||||
let inputMedia: Parameters<AppMessagesManager['sendOther']>[1], messageMedia: MessageMedia;
|
||||
const sendMessage = inlineResult.send_message;
|
||||
switch(sendMessage._) {
|
||||
case 'botInlineMessageMediaAuto': {
|
||||
@ -342,18 +343,50 @@ export class AppInlineBotsManager extends AppManager {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'botInlineMessageMediaInvoice': {
|
||||
// const photo = sendMessage.photo;
|
||||
// inputMedia = {
|
||||
// _: 'inputMediaInvoice',
|
||||
// description: sendMessage.description,
|
||||
// title: sendMessage.title,
|
||||
// photo: photo && {
|
||||
// _: 'inputWebDocument',
|
||||
// attributes: photo.attributes,
|
||||
// mime_type: photo.mime_type,
|
||||
// size: photo.size,
|
||||
// url: photo.url
|
||||
// },
|
||||
// invoice: undefined,
|
||||
// payload: undefined,
|
||||
// provider: undefined,
|
||||
// provider_data: undefined,
|
||||
// start_param: undefined
|
||||
// };
|
||||
|
||||
messageMedia = {
|
||||
_: 'messageMediaInvoice',
|
||||
title: sendMessage.title,
|
||||
description: sendMessage.description,
|
||||
photo: sendMessage.photo,
|
||||
currency: sendMessage.currency,
|
||||
total_amount: sendMessage.total_amount,
|
||||
pFlags: {
|
||||
shipping_address_requested: sendMessage.pFlags.shipping_address_requested,
|
||||
test: sendMessage.pFlags.test
|
||||
},
|
||||
start_param: undefined
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!inputMedia) {
|
||||
if(!inputMedia && messageMedia) {
|
||||
inputMedia = {
|
||||
_: 'messageMediaPending',
|
||||
type: inlineResult.type,
|
||||
file_name: inlineResult.title ||
|
||||
(inlineResult as BotInlineResult.botInlineResult).content?.url ||
|
||||
(inlineResult as BotInlineResult.botInlineResult).url,
|
||||
size: 0,
|
||||
progress: {percent: 30, total: 0}
|
||||
} as any;
|
||||
messageMedia
|
||||
};
|
||||
}
|
||||
|
||||
this.appMessagesManager.sendOther(peerId, inputMedia, options);
|
||||
|
@ -1250,7 +1250,7 @@ export class AppMessagesManager extends AppManager {
|
||||
return this.sendOther(peerId, this.appUsersManager.getContactMediaInput(contactPeerId));
|
||||
}
|
||||
|
||||
public sendOther(peerId: PeerId, inputMedia: InputMedia, options: Partial<{
|
||||
public sendOther(peerId: PeerId, inputMedia: InputMedia | {_: 'messageMediaPending', messageMedia: MessageMedia}, options: Partial<{
|
||||
replyToMsgId: number,
|
||||
threadId: number,
|
||||
viaBotId: BotId,
|
||||
@ -1345,9 +1345,8 @@ export class AppMessagesManager extends AppManager {
|
||||
break;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
case 'messageMediaPending': {
|
||||
media = inputMedia;
|
||||
media = (inputMedia as any).messageMedia;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1393,7 +1392,7 @@ export class AppMessagesManager extends AppManager {
|
||||
} else {
|
||||
apiPromise = this.apiManager.invokeApiAfter('messages.sendMedia', {
|
||||
peer: this.appPeersManager.getInputPeerById(peerId),
|
||||
media: inputMedia,
|
||||
media: inputMedia as InputMedia,
|
||||
random_id: message.random_id,
|
||||
reply_to_msg_id: replyToMsgId || undefined,
|
||||
message: '',
|
||||
@ -4032,6 +4031,13 @@ export class AppMessagesManager extends AppManager {
|
||||
this.onUpdateNewMessage(update);
|
||||
}
|
||||
|
||||
if(message._ === 'messageService' && message.action._ === 'messageActionPaymentSent') {
|
||||
this.rootScope.dispatchEvent('payment_sent', {
|
||||
peerId: message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : message.peerId,
|
||||
mid: message.reply_to_mid
|
||||
});
|
||||
}
|
||||
|
||||
if(!dialog && !isLocalThreadUpdate) {
|
||||
let good = true;
|
||||
if(peerId.isAnyChat()) {
|
||||
@ -4971,6 +4977,16 @@ export class AppMessagesManager extends AppManager {
|
||||
|
||||
const tempMessage = this.getMessageFromStorage(storage, tempId);
|
||||
storage.delete(tempId);
|
||||
|
||||
if(!(tempMessage as Message.message).reply_markup && (message as Message.message).reply_markup) {
|
||||
setTimeout(() => { // TODO: refactor it to normal buttons adding
|
||||
if(!this.getMessageFromStorage(storage, message.mid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rootScope.dispatchEvent('message_edit', {storageKey: storage.key, peerId: message.peerId, mid: message.mid, message});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.handleReleasingMessage(tempMessage, storage);
|
||||
|
||||
|
75
src/lib/appManagers/appPaymentsManager.ts
Normal file
75
src/lib/appManagers/appPaymentsManager.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { InputPaymentCredentials, PaymentRequestedInfo, PaymentsPaymentForm } from "../../layer";
|
||||
import { AppManager } from "./manager";
|
||||
import getServerMessageId from "./utils/messageId/getServerMessageId";
|
||||
|
||||
export default class AppPaymentsManager extends AppManager {
|
||||
public getPaymentForm(peerId: PeerId, mid: number) {
|
||||
return this.apiManager.invokeApi('payments.getPaymentForm', {
|
||||
peer: this.appPeersManager.getInputPeerById(peerId),
|
||||
msg_id: getServerMessageId(mid)
|
||||
}).then((paymentForm) => {
|
||||
this.appUsersManager.saveApiUsers(paymentForm.users);
|
||||
|
||||
return paymentForm;
|
||||
});
|
||||
}
|
||||
|
||||
public getPaymentReceipt(peerId: PeerId, mid: number) {
|
||||
return this.apiManager.invokeApi('payments.getPaymentReceipt', {
|
||||
peer: this.appPeersManager.getInputPeerById(peerId),
|
||||
msg_id: getServerMessageId(mid)
|
||||
}).then((paymentForm) => {
|
||||
this.appUsersManager.saveApiUsers(paymentForm.users);
|
||||
|
||||
return paymentForm;
|
||||
});
|
||||
}
|
||||
|
||||
public validateRequestedInfo(peerId: PeerId, mid: number, info: PaymentRequestedInfo, save?: boolean) {
|
||||
return this.apiManager.invokeApi('payments.validateRequestedInfo', {
|
||||
save,
|
||||
peer: this.appPeersManager.getInputPeerById(peerId),
|
||||
msg_id: getServerMessageId(mid),
|
||||
info
|
||||
});
|
||||
}
|
||||
|
||||
public sendPaymentForm(
|
||||
peerId: PeerId,
|
||||
mid: number,
|
||||
formId: PaymentsPaymentForm['form_id'],
|
||||
requestedInfoId: string,
|
||||
shippingOptionId: string,
|
||||
credentials: InputPaymentCredentials,
|
||||
tipAmount?: number
|
||||
) {
|
||||
return this.apiManager.invokeApi('payments.sendPaymentForm', {
|
||||
form_id: formId,
|
||||
peer: this.appPeersManager.getInputPeerById(peerId),
|
||||
msg_id: getServerMessageId(mid),
|
||||
requested_info_id: requestedInfoId,
|
||||
shipping_option_id: shippingOptionId,
|
||||
credentials,
|
||||
tip_amount: tipAmount || undefined
|
||||
}).then((result) => {
|
||||
if(result._ === 'payments.paymentResult') {
|
||||
this.apiUpdatesManager.processUpdateMessage(result.updates);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public clearSavedInfo(info?: boolean, credentials?: boolean) {
|
||||
return this.apiManager.invokeApi('payments.clearSavedInfo', {
|
||||
info,
|
||||
credentials
|
||||
});
|
||||
}
|
||||
}
|
@ -242,7 +242,7 @@ export class AppPeersManager extends AppManager {
|
||||
}
|
||||
}
|
||||
|
||||
public getDeleteButtonText(peerId: PeerId): LangPackKey {
|
||||
public getDeleteButtonText(peerId: PeerId): Extract<LangPackKey, 'ChannelDelete' | 'ChatList.Context.LeaveChannel' | 'DeleteMega' | 'ChatList.Context.LeaveGroup' | 'ChatList.Context.DeleteChat'> {
|
||||
switch(this.getDialogType(peerId)) {
|
||||
case 'channel':
|
||||
return this.appChatsManager.hasRights(peerId.toChatId(), 'delete_chat') ? 'ChannelDelete' : 'ChatList.Context.LeaveChannel';
|
||||
|
@ -44,6 +44,7 @@ import cryptoMessagePort from "../crypto/cryptoMessagePort";
|
||||
import appStateManager from "./appStateManager";
|
||||
import filterUnique from "../../helpers/array/filterUnique";
|
||||
import AppWebDocsManager from "./appWebDocsManager";
|
||||
import AppPaymentsManager from "./appPaymentsManager";
|
||||
|
||||
export default function createManagers(appStoragesManager: AppStoragesManager, userId: UserId) {
|
||||
const managers = {
|
||||
@ -84,7 +85,8 @@ export default function createManagers(appStoragesManager: AppStoragesManager, u
|
||||
timeManager: new TimeManager,
|
||||
appStoragesManager: appStoragesManager,
|
||||
appStateManager: appStateManager,
|
||||
appWebDocsManager: new AppWebDocsManager
|
||||
appWebDocsManager: new AppWebDocsManager,
|
||||
appPaymentsManager: new AppPaymentsManager
|
||||
};
|
||||
|
||||
type T = typeof managers;
|
||||
|
@ -30,6 +30,7 @@ import type { AppInlineBotsManager } from "./appInlineBotsManager";
|
||||
import type { AppMessagesIdsManager } from "./appMessagesIdsManager";
|
||||
import type { AppMessagesManager } from "./appMessagesManager";
|
||||
import type { AppNotificationsManager } from "./appNotificationsManager";
|
||||
import type AppPaymentsManager from "./appPaymentsManager";
|
||||
import type { AppPeersManager } from "./appPeersManager";
|
||||
import type { AppPhotosManager } from "./appPhotosManager";
|
||||
import type { AppPollsManager } from "./appPollsManager";
|
||||
@ -84,6 +85,7 @@ export class AppManager {
|
||||
protected appStoragesManager: AppStoragesManager;
|
||||
protected appStateManager: AppStateManager;
|
||||
protected appWebDocsManager: AppWebDocsManager;
|
||||
protected appPaymentsManager: AppPaymentsManager;
|
||||
|
||||
public clear: (init?: boolean) => void;
|
||||
|
||||
|
@ -4,10 +4,10 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { fontFamily } from "../../components/middleEllipsis";
|
||||
import getPeerTitle from "../../components/wrappers/getPeerTitle";
|
||||
import wrapMessageForReply from "../../components/wrappers/messageForReply";
|
||||
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||
import { FontFamily } from "../../config/font";
|
||||
import { IS_MOBILE } from "../../environment/userAgent";
|
||||
import IS_VIBRATE_SUPPORTED from "../../environment/vibrateSupport";
|
||||
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
|
||||
@ -343,7 +343,7 @@ export class UiNotificationsManager {
|
||||
|
||||
fontSize *= window.devicePixelRatio;
|
||||
|
||||
ctx.font = `700 ${fontSize}px ${fontFamily}`;
|
||||
ctx.font = `700 ${fontSize}px ${FontFamily}`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = 'white';
|
||||
|
@ -275,6 +275,10 @@ export class ApiFileManager extends AppManager {
|
||||
}
|
||||
|
||||
private getLimitPart(size: number): number {
|
||||
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
|
||||
return 512 * 1024;
|
||||
}
|
||||
|
||||
let bytes = 128 * 1024;
|
||||
|
||||
while((size / bytes) > 2000) {
|
||||
|
@ -72,8 +72,12 @@ export class PasswordManager extends AppManager {
|
||||
});
|
||||
}
|
||||
|
||||
public getInputCheckPassword(password: string, state: AccountPassword) {
|
||||
return this.cryptoWorker.invokeCrypto('computeSRP', password, state, false) as Promise<InputCheckPasswordSRP.inputCheckPasswordSRP>;
|
||||
}
|
||||
|
||||
public check(password: string, state: AccountPassword, options: any = {}) {
|
||||
return this.cryptoWorker.invokeCrypto('computeSRP', password, state, false).then((inputCheckPassword) => {
|
||||
return this.getInputCheckPassword(password, state).then((inputCheckPassword) => {
|
||||
//console.log('SRP', inputCheckPassword);
|
||||
return this.apiManager.invokeApi('auth.checkPassword', {
|
||||
password: inputCheckPassword as InputCheckPasswordSRP.inputCheckPasswordSRP
|
||||
|
@ -12,6 +12,7 @@
|
||||
import App from "../../config/app";
|
||||
import { MOUNT_CLASS_TO } from "../../config/debug";
|
||||
import Modes from "../../config/modes";
|
||||
import loadScript from "../../helpers/dom/loadScript";
|
||||
import tsNow from "../../helpers/tsNow";
|
||||
import sessionStorage from '../sessionStorage';
|
||||
|
||||
@ -47,16 +48,9 @@ export class TelegramMeWebManager {
|
||||
];
|
||||
|
||||
const promises = urls.map((url) => {
|
||||
const script = document.createElement('script');
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
script.onload = script.onerror = () => {
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
return loadScript(url).then((script) => {
|
||||
script.remove();
|
||||
});
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
return promise;
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
|
@ -12,7 +12,7 @@
|
||||
import sessionStorage from '../sessionStorage';
|
||||
import { nextRandomUint } from '../../helpers/random';
|
||||
import { WorkerTaskVoidTemplate } from '../../types';
|
||||
import longFromInts from '../../helpers/long/longFromInts';
|
||||
import ulongFromInts from '../../helpers/long/ulongFromInts';
|
||||
import { AppManager } from '../appManagers/manager';
|
||||
|
||||
/*
|
||||
@ -85,7 +85,7 @@ export class TimeManager extends AppManager {
|
||||
|
||||
this.lastMessageId = messageId;
|
||||
|
||||
const ret = longFromInts(messageId[0], messageId[1]);
|
||||
const ret = ulongFromInts(messageId[0], messageId[1]).toString(10);
|
||||
|
||||
// if(lol[ret]) {
|
||||
// console.error('[TimeManager]: Generated SAME msg id', messageId, this.timeOffset, ret);
|
||||
|
@ -16,25 +16,16 @@ import bytesToHex from '../../helpers/bytes/bytesToHex';
|
||||
import isObject from '../../helpers/object/isObject';
|
||||
import gzipUncompress from '../../helpers/gzipUncompress';
|
||||
import bigInt from 'big-integer';
|
||||
import longFromInts from '../../helpers/long/longFromInts';
|
||||
|
||||
// @ts-ignore
|
||||
/* import {BigInteger} from 'jsbn';
|
||||
|
||||
export function bigint(num: number) {
|
||||
return new BigInteger(num.toString(16), 16);
|
||||
}
|
||||
|
||||
function bigStringInt(strNum: string) {
|
||||
return new BigInteger(strNum, 10)
|
||||
} */
|
||||
import ulongFromInts from '../../helpers/long/ulongFromInts';
|
||||
|
||||
const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id;
|
||||
const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id;
|
||||
const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id;
|
||||
const gzipPacked = +Schema.MTProto.constructors.find((c) => c.predicate === 'gzip_packed').id;
|
||||
|
||||
//console.log('boolFalse', boolFalse === 0xbc799737);
|
||||
const safeBigInt = bigInt(Number.MAX_SAFE_INTEGER);
|
||||
const ulongBigInt = bigInt(bigInt[2]).pow(64);
|
||||
const longBigInt = ulongBigInt.divide(bigInt[2]);
|
||||
|
||||
class TLSerialization {
|
||||
private maxLength = 2048; // 2Kb
|
||||
@ -159,12 +150,13 @@ class TLSerialization {
|
||||
return this.storeIntBytes(sLong, 64, field);
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof sLong !== 'string') {
|
||||
sLong = sLong ? sLong.toString() : '0';
|
||||
|
||||
let _bigInt = bigInt(sLong as string);
|
||||
if(_bigInt.isNegative()) { // make it unsigned
|
||||
_bigInt = ulongBigInt.add(_bigInt);
|
||||
}
|
||||
|
||||
const {quotient, remainder} = bigInt(sLong).divmod(0x100000000);
|
||||
const {quotient, remainder} = _bigInt.divmod(0x100000000);
|
||||
const high = quotient.toJSNumber();
|
||||
const low = remainder.toJSNumber();
|
||||
|
||||
@ -506,23 +498,25 @@ class TLDeserialization<FetchLongAs extends Long> {
|
||||
return doubleView[0];
|
||||
}
|
||||
|
||||
// ! it should've been signed
|
||||
public fetchLong(field?: string): FetchLongAs {
|
||||
const iLow = this.readInt((field || '') + ':long[low]');
|
||||
const iHigh = this.readInt((field || '') + ':long[high]');
|
||||
|
||||
//const longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString();
|
||||
const longDec = longFromInts(iHigh, iLow);
|
||||
|
||||
let ulong = ulongFromInts(iHigh, iLow);
|
||||
if(/* !unsigned && */!this.mtproto && ulong.greater(longBigInt)) { // make it signed
|
||||
ulong = ulong.minus(ulongBigInt);
|
||||
}
|
||||
|
||||
if(!this.mtproto) {
|
||||
const num = +longDec;
|
||||
if(Number.isSafeInteger(num)) {
|
||||
if(safeBigInt.greaterOrEquals(ulong.abs())) {
|
||||
// @ts-ignore
|
||||
return num;
|
||||
return ulong.toJSNumber();
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return longDec;
|
||||
return ulong.toString(10);
|
||||
}
|
||||
|
||||
public fetchBool(field?: string): boolean {
|
||||
|
@ -136,7 +136,9 @@ export type BroadcastEvents = {
|
||||
|
||||
'service_notification': Update.updateServiceNotification,
|
||||
|
||||
'logging_out': void
|
||||
'logging_out': void,
|
||||
|
||||
'payment_sent': {peerId: PeerId, mid: number}
|
||||
};
|
||||
|
||||
export type BroadcastEventsListeners = {
|
||||
|
@ -19,8 +19,6 @@ import ripple from "../components/ripple";
|
||||
import findUpTag from "../helpers/dom/findUpTag";
|
||||
import findUpClassName from "../helpers/dom/findUpClassName";
|
||||
import { randomLong } from "../helpers/random";
|
||||
import AppStorage from "../lib/storage";
|
||||
import CacheStorageController from "../lib/cacheStorage";
|
||||
import pageSignQR from "./pageSignQR";
|
||||
import getLanguageChangeButton from "../components/languageChangeButton";
|
||||
import cancelEvent from "../helpers/dom/cancelEvent";
|
||||
@ -40,6 +38,7 @@ import IS_EMOJI_SUPPORTED from "../environment/emojiSupport";
|
||||
import setInnerHTML from "../helpers/dom/setInnerHTML";
|
||||
import wrapEmojiText from "../lib/richTextProcessor/wrapEmojiText";
|
||||
import apiManagerProxy from "../lib/mtproto/mtprotoworker";
|
||||
import CountryInputField from "../components/countryInputField";
|
||||
|
||||
//import _countries from '../countries_pretty.json';
|
||||
let btnNext: HTMLButtonElement = null, btnQr: HTMLButtonElement;
|
||||
@ -63,229 +62,25 @@ let onFirstMount = () => {
|
||||
//const countries: Country[] = _countries.default.filter((c) => c.emoji);
|
||||
// const countries: Country[] = Countries.filter((c) => c.emoji).sort((a, b) => a.name.localeCompare(b.name));
|
||||
// const countries = I18n.countriesList.filter((country) => !country.pFlags?.hidden);
|
||||
const setCountries = () => {
|
||||
countries = I18n.countriesList
|
||||
.filter((country) => !country.pFlags?.hidden)
|
||||
.sort((a, b) => (a.name || a.default_name).localeCompare(b.name || b.default_name));
|
||||
};
|
||||
let countries: HelpCountry.helpCountry[];
|
||||
|
||||
setCountries();
|
||||
|
||||
rootScope.addEventListener('language_change', () => {
|
||||
setCountries();
|
||||
});
|
||||
|
||||
const liMap: Map<string, HTMLLIElement[]> = new Map();
|
||||
|
||||
let lastCountrySelected: HelpCountry, lastCountryCodeSelected: HelpCountryCode;
|
||||
|
||||
const inputWrapper = document.createElement('div');
|
||||
inputWrapper.classList.add('input-wrapper');
|
||||
|
||||
const countryInputField = new InputField({
|
||||
label: 'Login.CountrySelectorLabel',
|
||||
name: randomLong()
|
||||
});
|
||||
let lastCountrySelected: HelpCountry, lastCountryCodeSelected: HelpCountryCode;
|
||||
const countryInputField = new CountryInputField({
|
||||
onCountryChange: (country, code) => {
|
||||
lastCountrySelected = country, lastCountryCodeSelected = code;
|
||||
|
||||
countryInputField.container.classList.add('input-select');
|
||||
|
||||
const countryInput = countryInputField.input;
|
||||
// countryInput.autocomplete = randomLong();
|
||||
|
||||
const selectWrapper = document.createElement('div');
|
||||
selectWrapper.classList.add('select-wrapper', 'z-depth-3', 'hide');
|
||||
|
||||
const arrowDown = document.createElement('span');
|
||||
arrowDown.classList.add('arrow', 'arrow-down');
|
||||
countryInputField.container.append(arrowDown);
|
||||
|
||||
const selectList = document.createElement('ul');
|
||||
selectWrapper.appendChild(selectList);
|
||||
|
||||
const scroll = new Scrollable(selectWrapper);
|
||||
|
||||
let initSelect = () => {
|
||||
initSelect = null;
|
||||
|
||||
countries.forEach((c) => {
|
||||
const emoji = getCountryEmoji(c.iso2);
|
||||
|
||||
const liArr: Array<HTMLLIElement> = [];
|
||||
c.country_codes.forEach((countryCode) => {
|
||||
const li = document.createElement('li');
|
||||
|
||||
let wrapped = wrapEmojiText(emoji);
|
||||
if(IS_EMOJI_SUPPORTED) {
|
||||
const spanEmoji = document.createElement('span');
|
||||
setInnerHTML(spanEmoji, wrapped);
|
||||
li.append(spanEmoji);
|
||||
} else {
|
||||
setInnerHTML(li, wrapped);
|
||||
}
|
||||
|
||||
const el = i18n(c.default_name as any);
|
||||
el.dataset.defaultName = c.default_name;
|
||||
li.append(el);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('phone-code');
|
||||
span.innerText = '+' + countryCode.country_code;
|
||||
li.appendChild(span);
|
||||
|
||||
liArr.push(li);
|
||||
selectList.append(li);
|
||||
});
|
||||
|
||||
liMap.set(c.iso2, liArr);
|
||||
});
|
||||
|
||||
selectList.addEventListener('mousedown', (e) => {
|
||||
if(e.button !== 0) { // other buttons but left shall not pass
|
||||
if(!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findUpTag(e.target, 'LI')
|
||||
selectCountryByTarget(target);
|
||||
//console.log('clicked', e, countryName, phoneCode);
|
||||
});
|
||||
|
||||
countryInputField.container.appendChild(selectWrapper);
|
||||
};
|
||||
|
||||
const selectCountryByTarget = (target: HTMLElement) => {
|
||||
const defaultName = (target.childNodes[1] as HTMLElement).dataset.defaultName;
|
||||
const phoneCode = target.querySelector<HTMLElement>('.phone-code').innerText;
|
||||
const countryCode = phoneCode.replace(/\D/g, '');
|
||||
|
||||
replaceContent(countryInput, i18n(defaultName as any));
|
||||
simulateEvent(countryInput, 'input');
|
||||
lastCountrySelected = countries.find((c) => c.default_name === defaultName);
|
||||
lastCountryCodeSelected = lastCountrySelected.country_codes.find((_countryCode) => _countryCode.country_code === countryCode);
|
||||
|
||||
telInputField.value = telInputField.lastValue = phoneCode;
|
||||
hidePicker();
|
||||
setTimeout(() => {
|
||||
telEl.focus();
|
||||
placeCaretAtEnd(telEl, true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
initSelect();
|
||||
|
||||
let hideTimeout: number;
|
||||
|
||||
countryInput.addEventListener('focus', function(this: typeof countryInput, e) {
|
||||
if(initSelect) {
|
||||
initSelect();
|
||||
} else {
|
||||
countries.forEach((c) => {
|
||||
liMap.get(c.iso2).forEach((li) => li.style.display = '');
|
||||
});
|
||||
telInputField.value = telInputField.lastValue = '+' + code.country_code;
|
||||
setTimeout(() => {
|
||||
telEl.focus();
|
||||
placeCaretAtEnd(telEl, true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = undefined;
|
||||
|
||||
selectWrapper.classList.remove('hide');
|
||||
void selectWrapper.offsetWidth; // reflow
|
||||
selectWrapper.classList.add('active');
|
||||
|
||||
countryInputField.select();
|
||||
|
||||
fastSmoothScroll({
|
||||
container: page.pageEl.parentElement.parentElement,
|
||||
element: countryInput,
|
||||
position: 'start',
|
||||
margin: 4
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if(!mouseDownHandlerAttached) {
|
||||
document.addEventListener('mousedown', onMouseDown, {capture: true});
|
||||
mouseDownHandlerAttached = true;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
let mouseDownHandlerAttached = false;
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if(findUpClassName(e.target, 'input-select')) {
|
||||
return;
|
||||
}
|
||||
if(e.target === countryInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
hidePicker();
|
||||
document.removeEventListener('mousedown', onMouseDown, {capture: true});
|
||||
mouseDownHandlerAttached = false;
|
||||
};
|
||||
|
||||
const hidePicker = () => {
|
||||
if(hideTimeout !== undefined) return;
|
||||
selectWrapper.classList.remove('active');
|
||||
hideTimeout = window.setTimeout(() => {
|
||||
selectWrapper.classList.add('hide');
|
||||
hideTimeout = undefined;
|
||||
}, 200);
|
||||
};
|
||||
/* false && countryInput.addEventListener('blur', function(this: typeof countryInput, e) {
|
||||
hidePicker();
|
||||
|
||||
e.cancelBubble = true;
|
||||
}, {capture: true}); */
|
||||
|
||||
countryInput.addEventListener('keyup', (e) => {
|
||||
const key = e.key;
|
||||
if(e.ctrlKey || key === 'Control') return false;
|
||||
|
||||
//let i = new RegExp('^' + this.value, 'i');
|
||||
let _value = countryInputField.value.toLowerCase();
|
||||
let matches: HelpCountry[] = [];
|
||||
countries.forEach((c) => {
|
||||
const names = [
|
||||
c.name,
|
||||
c.default_name,
|
||||
c.iso2
|
||||
];
|
||||
|
||||
names.filter(Boolean).forEach((name) => {
|
||||
const abbr = name.split(' ').filter((word) => /\w/.test(word)).map((word) => word[0]).join('');
|
||||
if(abbr.length > 1) {
|
||||
names.push(abbr);
|
||||
}
|
||||
});
|
||||
|
||||
let good = !!names.filter(Boolean).find((str) => str.toLowerCase().indexOf(_value) !== -1)/* === 0 */;//i.test(c.name);
|
||||
|
||||
liMap.get(c.iso2).forEach((li) => li.style.display = good ? '' : 'none');
|
||||
if(good) matches.push(c);
|
||||
});
|
||||
|
||||
// Код ниже автоматически выберет страну если она осталась одна при поиске
|
||||
/* if(matches.length === 1 && matches[0].li.length === 1) {
|
||||
if(matches[0].name === lastCountrySelected) return false;
|
||||
//console.log('clicking', matches[0]);
|
||||
|
||||
var clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initEvent('mousedown', true, true);
|
||||
matches[0].li[0].dispatchEvent(clickEvent);
|
||||
return false;
|
||||
} else */if(matches.length === 0) {
|
||||
countries.forEach((c) => {
|
||||
liMap.get(c.iso2).forEach((li) => li.style.display = '');
|
||||
});
|
||||
} else if(matches.length === 1 && key === 'Enter') {
|
||||
selectCountryByTarget(liMap.get(matches[0].iso2)[0]);
|
||||
}
|
||||
});
|
||||
|
||||
arrowDown.addEventListener('mousedown', function(this: typeof arrowDown, e) {
|
||||
e.cancelBubble = true;
|
||||
e.preventDefault();
|
||||
if(countryInput.matches(':focus')) countryInput.blur();
|
||||
else countryInput.focus();
|
||||
});
|
||||
|
||||
const telInputField = new TelInputField({
|
||||
@ -303,9 +98,7 @@ let onFirstMount = () => {
|
||||
)
|
||||
)
|
||||
) {
|
||||
replaceContent(countryInput, country ? i18n(country.default_name as any) : countryName);
|
||||
lastCountrySelected = country;
|
||||
lastCountryCodeSelected = code;
|
||||
countryInputField.override(country, code, countryName);
|
||||
}
|
||||
|
||||
//if(country && (telInputField.value.length - 1) >= (country.pattern ? country.pattern.length : 9)) {
|
||||
@ -485,7 +278,7 @@ let onFirstMount = () => {
|
||||
return nearestDcResult;
|
||||
}).then((nearestDcResult) => {
|
||||
if(!countryInputField.value.length && !telInputField.value.length) {
|
||||
selectCountryByTarget(liMap.get(nearestDcResult.country)[0]);
|
||||
countryInputField.selectCountryByIso2(nearestDcResult.country);
|
||||
}
|
||||
|
||||
//console.log('woohoo', nearestDcResult, country);
|
||||
|
@ -25,6 +25,14 @@ a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
-webkit-user-drag: none;
|
||||
|
@ -45,7 +45,9 @@
|
||||
@mixin btn-hoverable {
|
||||
@include hover-background-effect();
|
||||
|
||||
&.primary, &.blue, &.active {
|
||||
&.primary,
|
||||
&.blue,
|
||||
&.active {
|
||||
@include hover-background-effect(primary);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,8 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
.btn, .btn-icon {
|
||||
.btn,
|
||||
.btn-icon {
|
||||
background: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
|
@ -2147,6 +2147,12 @@ $bubble-beside-button-width: 38px;
|
||||
code {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.is-invoice {
|
||||
.attachment {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * fix scroll with only 1 bubble
|
||||
@ -2851,6 +2857,10 @@ $bubble-beside-button-width: 38px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&.is-buy:before {
|
||||
content: $tgico-card;
|
||||
}
|
||||
|
||||
&.is-switch-inline:before {
|
||||
content: $tgico-forward_filled;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
.checkbox-field {
|
||||
--size: 1.25rem;
|
||||
--offset-left: 0px;
|
||||
margin: 1.5rem 1.1875rem;
|
||||
display: block;
|
||||
text-align: left;
|
||||
@ -29,19 +30,21 @@
|
||||
|
||||
.checkbox-box {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
left: var(--offset-left);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: .25rem;
|
||||
border-radius: .3125rem;
|
||||
overflow: hidden;
|
||||
|
||||
html.is-safari & {
|
||||
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow
|
||||
}
|
||||
|
||||
&-check, &-background, &-border {
|
||||
&-check,
|
||||
&-background,
|
||||
&-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -73,7 +76,7 @@
|
||||
}
|
||||
|
||||
&-check {
|
||||
--offset: 3px;
|
||||
--offset: 7px;
|
||||
width: calc(var(--size) - var(--offset));
|
||||
height: calc(var(--size) - var(--offset));
|
||||
top: 50%;
|
||||
@ -82,7 +85,7 @@
|
||||
|
||||
use {
|
||||
stroke: #fff;
|
||||
stroke-width: 2.75;
|
||||
stroke-width: 3.75;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 24.19, 24.19;
|
||||
stroke-dashoffset: 0;
|
||||
@ -99,15 +102,10 @@
|
||||
.checkbox-caption {
|
||||
position: relative;
|
||||
padding-left: 3.375rem;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
min-height: 24px;
|
||||
margin-top: 1px;
|
||||
line-height: 26px;
|
||||
user-select: none;
|
||||
transition: .2s opacity;
|
||||
// color: var(--primary-text-color);
|
||||
color: inherit;
|
||||
pointer-events: none;
|
||||
line-height: var(--line-height);
|
||||
|
||||
@include animation-level(0) {
|
||||
transition: none;
|
||||
@ -168,6 +166,7 @@
|
||||
|
||||
.radio-field {
|
||||
--size: 1.375rem;
|
||||
--offset-left: 0px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
margin: 1.25rem 0;
|
||||
@ -210,7 +209,7 @@
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
left: var(--offset-left);
|
||||
top: 50%;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
@ -228,7 +227,7 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: .3125rem;
|
||||
left: calc(var(--offset-left) + .3125rem);
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
border-radius: 50%;
|
||||
|
@ -81,6 +81,18 @@
|
||||
transition: opacity .2s;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: .375rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-input {
|
||||
--padding: 1rem;
|
||||
@ -90,7 +102,7 @@
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--surface-color);
|
||||
//padding: 0 1rem;
|
||||
padding: calc(var(--padding) - var(--border-width));
|
||||
padding: calc(var(--padding) - var(--border-width)) calc(var(--padding-horizontal) - var(--border-width));
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: var(--height);
|
||||
@ -123,6 +135,7 @@
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
--padding: .9375rem;
|
||||
--padding-horizontal: .9375rem;
|
||||
}
|
||||
|
||||
@include animation-level(0) {
|
||||
@ -215,8 +228,8 @@
|
||||
&:valid ~ label,
|
||||
&:not(:empty) ~ label,
|
||||
&:disabled ~ label {
|
||||
transform: translate(-.25rem, calc(var(--height) / -2 + .125rem)) scale(.75);
|
||||
padding: 0 6px;
|
||||
transform: translate(-.1875rem, calc(var(--height) / -2 + .0625rem)) scale(.75);
|
||||
padding: 0 .3125rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -237,6 +250,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.input-fields-row {
|
||||
display: flex;
|
||||
|
||||
.input-field {
|
||||
flex: 1 1 auto;
|
||||
width: 1%; // fix width because of contenteditable
|
||||
|
||||
// &:not(:first-child) {
|
||||
// margin-left: 1rem;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper > * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
@ -4,10 +4,12 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
$row-border-radius: $border-radius-medium;
|
||||
|
||||
.row {
|
||||
min-height: 3.5rem;
|
||||
position: relative;
|
||||
padding: .6875rem 1rem;
|
||||
padding: .4375rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@ -80,7 +82,7 @@
|
||||
margin-top: -.125rem;
|
||||
}
|
||||
|
||||
.row-subtitle:not(:empty) + .row-title.tgico:before {
|
||||
.row-subtitle:not(:empty):not(.hide) + .row-title.tgico:before {
|
||||
margin-top: .25rem;
|
||||
}
|
||||
}
|
||||
@ -90,7 +92,7 @@
|
||||
overflow: hidden;
|
||||
|
||||
@include respond-to(not-handhelds) {
|
||||
border-radius: $border-radius-medium;
|
||||
border-radius: $row-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,6 +102,22 @@
|
||||
margin-left: -3.375rem;
|
||||
}
|
||||
|
||||
.radio-field,
|
||||
.radio-field-main,
|
||||
.checkbox-field {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.radio-field,
|
||||
.checkbox-field {
|
||||
--offset-left: 1rem;
|
||||
}
|
||||
|
||||
.radio-field {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
margin-right: 0;
|
||||
height: auto;
|
||||
@ -134,6 +152,9 @@
|
||||
position: absolute !important;
|
||||
margin: 0 !important;
|
||||
left: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-small {
|
||||
width: 2rem !important;
|
||||
|
@ -96,6 +96,23 @@ html:not(.is-safari):not(.is-ios) {
|
||||
scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
|
||||
-ms-overflow-style: none;
|
||||
transform: translateZ(0);
|
||||
|
||||
&.scrollable-y-bordered {
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: border-top-color var(--transition-standard-in), border-bottom-color var(--transition-standard-in);
|
||||
}
|
||||
|
||||
&:not(.scrolled-top) {
|
||||
border-top-color: var(--border-color);
|
||||
}
|
||||
|
||||
&:not(.scrolled-bottom) {
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
// html.is-firefox & {
|
||||
// transition: scrollbar-color .3s ease;
|
||||
|
@ -29,7 +29,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hidden-widget, .radio-field:first-child:last-child {
|
||||
.hidden-widget,
|
||||
.radio-field:first-child:last-child {
|
||||
.btn-icon {
|
||||
pointer-events: none;
|
||||
opacity: 0 !important;
|
||||
@ -97,4 +98,8 @@
|
||||
top: 54px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
hr:not(.hide) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,6 @@
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding: 0 1.375rem;
|
||||
margin-top: -3px;
|
||||
border-radius: $border-radius-medium;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@ -48,15 +47,6 @@
|
||||
margin: -1px 0 0 -4px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
flex: 1;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0;
|
||||
margin-top: -3px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
&-photo {
|
||||
max-width: 380px;
|
||||
overflow: hidden;
|
||||
|
@ -6,19 +6,6 @@
|
||||
|
||||
.popup-mute {
|
||||
.popup-container {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
margin: 0 -.625rem;
|
||||
}
|
||||
|
||||
.sidebar-left-section {
|
||||
margin-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
&-content {
|
||||
margin: 0 !important;
|
||||
}
|
||||
min-width: 16rem;
|
||||
}
|
||||
}
|
||||
|
249
src/scss/partials/popups/_payment.scss
Normal file
249
src/scss/partials/popups/_payment.scss
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
.popup-payment {
|
||||
$parent: ".popup";
|
||||
|
||||
#{$parent} {
|
||||
&-container {
|
||||
padding: 0;
|
||||
width: 26.25rem;
|
||||
max-width: 26.25rem;
|
||||
|
||||
max-height: unquote('min(100%, 43.5rem)');
|
||||
border-radius: $border-radius-huge;
|
||||
}
|
||||
|
||||
&-header {
|
||||
height: 3.5rem;
|
||||
margin: 0;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading .popup-container {
|
||||
min-height: 26.25rem;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
--height: 3rem;
|
||||
margin: 1.25rem .5rem 0;
|
||||
|
||||
&-input {
|
||||
--padding: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-left-section {
|
||||
padding: 0 !important;
|
||||
|
||||
&-name + .input-field,
|
||||
&-name + .input-fields-row .input-field {
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
&-content {
|
||||
margin: 0 .5rem !important;
|
||||
}
|
||||
|
||||
// &-container {
|
||||
// &:last-child .sidebar-left-section {
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
max-height: 10rem;
|
||||
box-shadow: var(--menu-box-shadow);
|
||||
|
||||
li {
|
||||
grid-template-columns: calc(26px + 2rem) 1fr;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-verification {
|
||||
width: 100%;
|
||||
min-height: 30rem;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-radius: $row-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-item {
|
||||
width: 100%;
|
||||
padding: 0 1.25rem;
|
||||
|
||||
&-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
// height: 6.25rem;
|
||||
|
||||
// max-height: 100px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-photo {
|
||||
width: 6.25rem;
|
||||
height: 6.25rem;
|
||||
flex: 0 0 auto;
|
||||
border-radius: $border-radius-medium;
|
||||
margin-right: 1rem;
|
||||
// background-color: var(--secondary-color);
|
||||
|
||||
.media-photo {
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&-lines {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
// font-size: var(--font-size-20);
|
||||
// line-height: var(--line-height-20);
|
||||
// margin: 5px 0;
|
||||
|
||||
font-size: var(--font-size-16);
|
||||
line-height: var(--line-height-16);
|
||||
}
|
||||
|
||||
&-description,
|
||||
&-bot-name {
|
||||
// flex: 1 1 auto;
|
||||
font-size: var(--font-size-14);
|
||||
line-height: 1.25rem;
|
||||
// color: var(--secondary-text-color);
|
||||
|
||||
// @include text-overflow(false);
|
||||
}
|
||||
|
||||
&-bot-name {
|
||||
color: var(--secondary-text-color);
|
||||
// flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem .25rem;
|
||||
|
||||
&-price {
|
||||
color: var(--secondary-text-color);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 1.1875rem;
|
||||
|
||||
& + & {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&.is-total {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tips {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: .75rem -.5rem 1.5rem;
|
||||
|
||||
&-tip {
|
||||
--background-intensity: .1;
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
height: 2.5rem;
|
||||
border-radius: 1.25rem;
|
||||
background-color: rgba(84, 190, 97, var(--background-intensity));
|
||||
color: #3ba748;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-16);
|
||||
line-height: 2.5rem;
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: color .1s ease-in-out, background-color .1s ease-in-out;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
@include hover() {
|
||||
--background-intensity: .3;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
--background-intensity: 1;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
color: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
// text-align: right;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
margin: 0 .5rem;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&-method-row {
|
||||
.media-photo {
|
||||
border-radius: $border-radius-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&-pay {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
height: 3rem;
|
||||
margin: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-preloader-container {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
11
src/scss/partials/popups/_paymentCard.scss
Normal file
11
src/scss/partials/popups/_paymentCard.scss
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
// .popup-payment-card {
|
||||
// .popup-container {
|
||||
// max-height: 26.25rem;
|
||||
// }
|
||||
// }
|
15
src/scss/partials/popups/_paymentCardConfirmation.scss
Normal file
15
src/scss/partials/popups/_paymentCardConfirmation.scss
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
.popup-payment-card-confirmation {
|
||||
.popup-container {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.input-field-password {
|
||||
margin-top: .5rem !important;
|
||||
}
|
||||
}
|
15
src/scss/partials/popups/_paymentShippingMethods.scss
Normal file
15
src/scss/partials/popups/_paymentShippingMethods.scss
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
.popup-payment-shipping-methods {
|
||||
.popup-container {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
9
src/scss/partials/popups/_paymentVerification.scss
Normal file
9
src/scss/partials/popups/_paymentVerification.scss
Normal file
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
// .popup-payment-verification {
|
||||
|
||||
// }
|
@ -10,28 +10,24 @@
|
||||
#{$parent} {
|
||||
&-header {
|
||||
display: flex;
|
||||
margin-bottom: .625rem;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
height: 2.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-container {
|
||||
padding: 1rem 1.5rem .8125rem;
|
||||
padding: .75rem .5rem;
|
||||
max-width: unquote('min(400px, 100%)');
|
||||
|
||||
&.have-checkbox {
|
||||
.popup-buttons {
|
||||
margin-top: .5625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: .125rem;
|
||||
// margin-bottom: .125rem;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-left: .6875rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,30 +40,16 @@
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
margin-top: 1.625rem;
|
||||
margin-right: -.5rem;
|
||||
|
||||
.btn {
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
& + .btn {
|
||||
margin-top: .625rem;
|
||||
}
|
||||
}
|
||||
padding: .625rem 1rem .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3.5rem;
|
||||
/* padding: 0 .9375rem;
|
||||
margin: 0 -.8125rem; */
|
||||
padding: 0 1.1875rem;
|
||||
margin: 0 -1.0625rem;
|
||||
height: 3rem;
|
||||
margin: 0;
|
||||
padding: 0 1.125rem;
|
||||
|
||||
.checkbox-box {
|
||||
left: auto;
|
||||
|
@ -53,6 +53,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
flex: 1;
|
||||
padding: 0 2rem 0 1.5rem;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
--translateX: 0;
|
||||
position: relative;
|
||||
@ -109,38 +122,24 @@
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
&-row {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 3rem;
|
||||
padding: 0 .5rem;
|
||||
|
||||
.btn {
|
||||
& + .btn {
|
||||
margin-top: 0 !important;
|
||||
margin-right: 1.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: .5rem;
|
||||
padding: 0 1rem;
|
||||
text-transform: uppercase;
|
||||
border-radius: $border-radius;
|
||||
border-radius: $border-radius-medium;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
height: 2.5rem;
|
||||
@include text-overflow();
|
||||
|
||||
& + .btn {
|
||||
margin-top: .5rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: .625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,15 +12,5 @@
|
||||
width: calc(100% + 3rem);
|
||||
padding: .5rem 1.5rem;
|
||||
user-select: text;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:not(.scrolled-top) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
&:not(.scrolled-bottom) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,16 +8,7 @@
|
||||
$parent: ".popup";
|
||||
user-select: none;
|
||||
|
||||
h6 {
|
||||
padding: 0 2rem 0 1.5rem;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
.sticker-set-footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
|
||||
@ -48,14 +39,13 @@
|
||||
}
|
||||
|
||||
&-header {
|
||||
margin-bottom: 12px;
|
||||
margin: .625rem 0;
|
||||
flex: 0 0 auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-set {
|
||||
margin-bottom: 8px;
|
||||
margin: .0625rem 0;
|
||||
|
||||
&-stickers {
|
||||
padding: 0 5px;
|
||||
|
@ -86,9 +86,11 @@ $chat-input-inner-padding-handhelds: .25rem;
|
||||
--bubble-transition-in: transform var(--transition-standard-in), opacity var(--transition-standard-in);
|
||||
--bubble-transition-out: transform var(--transition-standard-out), opacity var(--transition-standard-out);
|
||||
--line-height: 1.3125;
|
||||
--line-height-20: 23px;
|
||||
--line-height-16: 21px;
|
||||
--line-height-14: 18px;
|
||||
--line-height-12: 16px;
|
||||
--font-size-20: 20px;
|
||||
--font-size-16: 16px;
|
||||
--font-size-14: 14px;
|
||||
--font-size-12: 12px;
|
||||
@ -369,6 +371,11 @@ $chat-input-inner-padding-handhelds: .25rem;
|
||||
@import "partials/popups/sponsored";
|
||||
@import "partials/popups/mute";
|
||||
@import "partials/popups/reactedList";
|
||||
@import "partials/popups/payment";
|
||||
@import "partials/popups/paymentCard";
|
||||
@import "partials/popups/paymentShippingMethods";
|
||||
@import "partials/popups/paymentVerification";
|
||||
@import "partials/popups/paymentCardConfirmation";
|
||||
|
||||
@import "partials/pages/pages";
|
||||
@import "partials/pages/authCode";
|
||||
@ -550,6 +557,11 @@ input::-webkit-credentials-auto-fill-button {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
input:-webkit-autofill::first-line {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
/* input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
@ -1256,6 +1268,16 @@ middle-ellipsis-element {
|
||||
// }
|
||||
// }
|
||||
|
||||
.media-container-cover {
|
||||
position: relative;
|
||||
|
||||
.media-photo {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.media-photo,
|
||||
.media-video,
|
||||
.media-sticker,
|
||||
|
@ -3,9 +3,9 @@
|
||||
@font-face {
|
||||
font-family: '#{$tgico-font-family}';
|
||||
src:
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?5o4186') format('truetype'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.woff?5o4186') format('woff'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.svg?5o4186##{$tgico-font-family}') format('svg');
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?js2svd') format('truetype'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.woff?js2svd') format('woff'),
|
||||
url('#{$tgico-font-path}/#{$tgico-font-family}.svg?js2svd##{$tgico-font-family}') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@ -167,6 +167,16 @@
|
||||
content: $tgico-car;
|
||||
}
|
||||
}
|
||||
.tgico-card {
|
||||
&:before {
|
||||
content: $tgico-card;
|
||||
}
|
||||
}
|
||||
.tgico-card_outline {
|
||||
&:before {
|
||||
content: $tgico-card_outline;
|
||||
}
|
||||
}
|
||||
.tgico-channel {
|
||||
&:before {
|
||||
content: $tgico-channel;
|
||||
@ -827,6 +837,11 @@
|
||||
content: $tgico-sharescreen_filled;
|
||||
}
|
||||
}
|
||||
.tgico-shipping {
|
||||
&:before {
|
||||
content: $tgico-shipping;
|
||||
}
|
||||
}
|
||||
.tgico-shuffle {
|
||||
&:before {
|
||||
content: $tgico-shuffle;
|
||||
|
@ -25,165 +25,168 @@ $tgico-calendarfilter: "\e917";
|
||||
$tgico-camera: "\e918";
|
||||
$tgico-cameraadd: "\e919";
|
||||
$tgico-car: "\e91a";
|
||||
$tgico-channel: "\e91b";
|
||||
$tgico-channelviews: "\e91c";
|
||||
$tgico-chatspinned: "\e91d";
|
||||
$tgico-chatsplaceholder: "\e91e";
|
||||
$tgico-check1: "\e91f";
|
||||
$tgico-checkbox: "\e920";
|
||||
$tgico-checkboxblock: "\e921";
|
||||
$tgico-checkboxempty: "\e922";
|
||||
$tgico-checkboxon: "\e923";
|
||||
$tgico-checkretract: "\e924";
|
||||
$tgico-checkround: "\e925";
|
||||
$tgico-close: "\e926";
|
||||
$tgico-clouddownload: "\e927";
|
||||
$tgico-colorize: "\e928";
|
||||
$tgico-comments: "\e929";
|
||||
$tgico-commentssticker: "\e92a";
|
||||
$tgico-copy: "\e92b";
|
||||
$tgico-darkmode: "\e92c";
|
||||
$tgico-data: "\e92d";
|
||||
$tgico-delete: "\e92e";
|
||||
$tgico-delete_filled: "\e92f";
|
||||
$tgico-deletedaccount: "\e930";
|
||||
$tgico-deleteleft: "\e931";
|
||||
$tgico-deleteuser: "\e932";
|
||||
$tgico-devices: "\e933";
|
||||
$tgico-document: "\e934";
|
||||
$tgico-down: "\e935";
|
||||
$tgico-download: "\e936";
|
||||
$tgico-dragfiles: "\e937";
|
||||
$tgico-dragmedia: "\e938";
|
||||
$tgico-eats: "\e939";
|
||||
$tgico-edit: "\e93a";
|
||||
$tgico-email: "\e93b";
|
||||
$tgico-endcall_filled: "\e93c";
|
||||
$tgico-enter: "\e93d";
|
||||
$tgico-eye1: "\e93e";
|
||||
$tgico-eye2: "\e93f";
|
||||
$tgico-fast_forward: "\e940";
|
||||
$tgico-fast_rewind: "\e941";
|
||||
$tgico-favourites: "\e942";
|
||||
$tgico-flag: "\e943";
|
||||
$tgico-flip: "\e944";
|
||||
$tgico-folder: "\e945";
|
||||
$tgico-fontsize: "\e946";
|
||||
$tgico-forward: "\e947";
|
||||
$tgico-forward_filled: "\e948";
|
||||
$tgico-fullscreen: "\e949";
|
||||
$tgico-gc_microphone: "\e94a";
|
||||
$tgico-gc_microphoneoff: "\e94b";
|
||||
$tgico-gifs: "\e94c";
|
||||
$tgico-group: "\e94d";
|
||||
$tgico-help: "\e94e";
|
||||
$tgico-image: "\e94f";
|
||||
$tgico-info: "\e950";
|
||||
$tgico-info2: "\e951";
|
||||
$tgico-italic: "\e952";
|
||||
$tgico-keyboard: "\e953";
|
||||
$tgico-lamp: "\e954";
|
||||
$tgico-language: "\e955";
|
||||
$tgico-largepause: "\e956";
|
||||
$tgico-largeplay: "\e957";
|
||||
$tgico-left: "\e958";
|
||||
$tgico-link: "\e959";
|
||||
$tgico-listscreenshare: "\e95a";
|
||||
$tgico-livelocation: "\e95b";
|
||||
$tgico-location: "\e95c";
|
||||
$tgico-lock: "\e95d";
|
||||
$tgico-lockoff: "\e95e";
|
||||
$tgico-loginlogodesktop: "\e95f";
|
||||
$tgico-loginlogomobile: "\e960";
|
||||
$tgico-logout: "\e961";
|
||||
$tgico-mention: "\e962";
|
||||
$tgico-menu: "\e963";
|
||||
$tgico-message: "\e964";
|
||||
$tgico-messageunread: "\e965";
|
||||
$tgico-microphone: "\e966";
|
||||
$tgico-microphone_crossed: "\e967";
|
||||
$tgico-microphone_crossed_filled: "\e968";
|
||||
$tgico-microphone_filled: "\e969";
|
||||
$tgico-minus: "\e96a";
|
||||
$tgico-monospace: "\e96b";
|
||||
$tgico-more: "\e96c";
|
||||
$tgico-mute: "\e96d";
|
||||
$tgico-muted: "\e96e";
|
||||
$tgico-newchannel: "\e96f";
|
||||
$tgico-newchat_filled: "\e970";
|
||||
$tgico-newgroup: "\e971";
|
||||
$tgico-newprivate: "\e972";
|
||||
$tgico-next: "\e973";
|
||||
$tgico-noncontacts: "\e974";
|
||||
$tgico-nosound: "\e975";
|
||||
$tgico-passwordoff: "\e976";
|
||||
$tgico-pause: "\e977";
|
||||
$tgico-permissions: "\e978";
|
||||
$tgico-phone: "\e979";
|
||||
$tgico-pin: "\e97a";
|
||||
$tgico-pinlist: "\e97b";
|
||||
$tgico-pinned_filled: "\e97c";
|
||||
$tgico-pinnedchat: "\e97d";
|
||||
$tgico-pip: "\e97e";
|
||||
$tgico-play: "\e97f";
|
||||
$tgico-playback_05: "\e980";
|
||||
$tgico-playback_15: "\e981";
|
||||
$tgico-playback_1x: "\e982";
|
||||
$tgico-playback_2x: "\e983";
|
||||
$tgico-plus: "\e984";
|
||||
$tgico-poll: "\e985";
|
||||
$tgico-previous: "\e986";
|
||||
$tgico-radiooff: "\e987";
|
||||
$tgico-radioon: "\e988";
|
||||
$tgico-reactions: "\e989";
|
||||
$tgico-readchats: "\e98a";
|
||||
$tgico-recent: "\e98b";
|
||||
$tgico-replace: "\e98c";
|
||||
$tgico-reply: "\e98d";
|
||||
$tgico-reply_filled: "\e98e";
|
||||
$tgico-rightpanel: "\e98f";
|
||||
$tgico-rotate_left: "\e990";
|
||||
$tgico-rotate_right: "\e991";
|
||||
$tgico-saved: "\e992";
|
||||
$tgico-savedmessages: "\e993";
|
||||
$tgico-schedule: "\e994";
|
||||
$tgico-scheduled: "\e995";
|
||||
$tgico-search: "\e996";
|
||||
$tgico-select: "\e997";
|
||||
$tgico-send: "\e998";
|
||||
$tgico-send2: "\e999";
|
||||
$tgico-sending: "\e99a";
|
||||
$tgico-sendingerror: "\e99b";
|
||||
$tgico-settings: "\e99c";
|
||||
$tgico-settings_filled: "\e99d";
|
||||
$tgico-sharescreen_filled: "\e99e";
|
||||
$tgico-shuffle: "\e99f";
|
||||
$tgico-smallscreen: "\e9a0";
|
||||
$tgico-smile: "\e9a1";
|
||||
$tgico-spoiler: "\e9a2";
|
||||
$tgico-sport: "\e9a3";
|
||||
$tgico-stickers: "\e9a4";
|
||||
$tgico-stop: "\e9a5";
|
||||
$tgico-strikethrough: "\e9a6";
|
||||
$tgico-textedit: "\e9a7";
|
||||
$tgico-tip: "\e9a8";
|
||||
$tgico-tools: "\e9a9";
|
||||
$tgico-unarchive: "\e9aa";
|
||||
$tgico-underline: "\e9ab";
|
||||
$tgico-unmute: "\e9ac";
|
||||
$tgico-unpin: "\e9ad";
|
||||
$tgico-unread: "\e9ae";
|
||||
$tgico-up: "\e9af";
|
||||
$tgico-user: "\e9b0";
|
||||
$tgico-username: "\e9b1";
|
||||
$tgico-videocamera: "\e9b2";
|
||||
$tgico-videocamera_crossed_filled: "\e9b3";
|
||||
$tgico-videocamera_filled: "\e9b4";
|
||||
$tgico-videochat: "\e9b5";
|
||||
$tgico-volume_down: "\e9b6";
|
||||
$tgico-volume_mute: "\e9b7";
|
||||
$tgico-volume_off: "\e9b8";
|
||||
$tgico-volume_up: "\e9b9";
|
||||
$tgico-zoomin: "\e9ba";
|
||||
$tgico-zoomout: "\e9bb";
|
||||
$tgico-card: "\e91b";
|
||||
$tgico-card_outline: "\e91c";
|
||||
$tgico-channel: "\e91d";
|
||||
$tgico-channelviews: "\e91e";
|
||||
$tgico-chatspinned: "\e91f";
|
||||
$tgico-chatsplaceholder: "\e920";
|
||||
$tgico-check1: "\e921";
|
||||
$tgico-checkbox: "\e922";
|
||||
$tgico-checkboxblock: "\e923";
|
||||
$tgico-checkboxempty: "\e924";
|
||||
$tgico-checkboxon: "\e925";
|
||||
$tgico-checkretract: "\e926";
|
||||
$tgico-checkround: "\e927";
|
||||
$tgico-close: "\e928";
|
||||
$tgico-clouddownload: "\e929";
|
||||
$tgico-colorize: "\e92a";
|
||||
$tgico-comments: "\e92b";
|
||||
$tgico-commentssticker: "\e92c";
|
||||
$tgico-copy: "\e92d";
|
||||
$tgico-darkmode: "\e92e";
|
||||
$tgico-data: "\e92f";
|
||||
$tgico-delete: "\e930";
|
||||
$tgico-delete_filled: "\e931";
|
||||
$tgico-deletedaccount: "\e932";
|
||||
$tgico-deleteleft: "\e933";
|
||||
$tgico-deleteuser: "\e934";
|
||||
$tgico-devices: "\e935";
|
||||
$tgico-document: "\e936";
|
||||
$tgico-down: "\e937";
|
||||
$tgico-download: "\e938";
|
||||
$tgico-dragfiles: "\e939";
|
||||
$tgico-dragmedia: "\e93a";
|
||||
$tgico-eats: "\e93b";
|
||||
$tgico-edit: "\e93c";
|
||||
$tgico-email: "\e93d";
|
||||
$tgico-endcall_filled: "\e93e";
|
||||
$tgico-enter: "\e93f";
|
||||
$tgico-eye1: "\e940";
|
||||
$tgico-eye2: "\e941";
|
||||
$tgico-fast_forward: "\e942";
|
||||
$tgico-fast_rewind: "\e943";
|
||||
$tgico-favourites: "\e944";
|
||||
$tgico-flag: "\e945";
|
||||
$tgico-flip: "\e946";
|
||||
$tgico-folder: "\e947";
|
||||
$tgico-fontsize: "\e948";
|
||||
$tgico-forward: "\e949";
|
||||
$tgico-forward_filled: "\e94a";
|
||||
$tgico-fullscreen: "\e94b";
|
||||
$tgico-gc_microphone: "\e94c";
|
||||
$tgico-gc_microphoneoff: "\e94d";
|
||||
$tgico-gifs: "\e94e";
|
||||
$tgico-group: "\e94f";
|
||||
$tgico-help: "\e950";
|
||||
$tgico-image: "\e951";
|
||||
$tgico-info: "\e952";
|
||||
$tgico-info2: "\e953";
|
||||
$tgico-italic: "\e954";
|
||||
$tgico-keyboard: "\e955";
|
||||
$tgico-lamp: "\e956";
|
||||
$tgico-language: "\e957";
|
||||
$tgico-largepause: "\e958";
|
||||
$tgico-largeplay: "\e959";
|
||||
$tgico-left: "\e95a";
|
||||
$tgico-link: "\e95b";
|
||||
$tgico-listscreenshare: "\e95c";
|
||||
$tgico-livelocation: "\e95d";
|
||||
$tgico-location: "\e95e";
|
||||
$tgico-lock: "\e95f";
|
||||
$tgico-lockoff: "\e960";
|
||||
$tgico-loginlogodesktop: "\e961";
|
||||
$tgico-loginlogomobile: "\e962";
|
||||
$tgico-logout: "\e963";
|
||||
$tgico-mention: "\e964";
|
||||
$tgico-menu: "\e965";
|
||||
$tgico-message: "\e966";
|
||||
$tgico-messageunread: "\e967";
|
||||
$tgico-microphone: "\e968";
|
||||
$tgico-microphone_crossed: "\e969";
|
||||
$tgico-microphone_crossed_filled: "\e96a";
|
||||
$tgico-microphone_filled: "\e96b";
|
||||
$tgico-minus: "\e96c";
|
||||
$tgico-monospace: "\e96d";
|
||||
$tgico-more: "\e96e";
|
||||
$tgico-mute: "\e96f";
|
||||
$tgico-muted: "\e970";
|
||||
$tgico-newchannel: "\e971";
|
||||
$tgico-newchat_filled: "\e972";
|
||||
$tgico-newgroup: "\e973";
|
||||
$tgico-newprivate: "\e974";
|
||||
$tgico-next: "\e975";
|
||||
$tgico-noncontacts: "\e976";
|
||||
$tgico-nosound: "\e977";
|
||||
$tgico-passwordoff: "\e978";
|
||||
$tgico-pause: "\e979";
|
||||
$tgico-permissions: "\e97a";
|
||||
$tgico-phone: "\e97b";
|
||||
$tgico-pin: "\e97c";
|
||||
$tgico-pinlist: "\e97d";
|
||||
$tgico-pinned_filled: "\e97e";
|
||||
$tgico-pinnedchat: "\e97f";
|
||||
$tgico-pip: "\e980";
|
||||
$tgico-play: "\e981";
|
||||
$tgico-playback_05: "\e982";
|
||||
$tgico-playback_15: "\e983";
|
||||
$tgico-playback_1x: "\e984";
|
||||
$tgico-playback_2x: "\e985";
|
||||
$tgico-plus: "\e986";
|
||||
$tgico-poll: "\e987";
|
||||
$tgico-previous: "\e988";
|
||||
$tgico-radiooff: "\e989";
|
||||
$tgico-radioon: "\e98a";
|
||||
$tgico-reactions: "\e98b";
|
||||
$tgico-readchats: "\e98c";
|
||||
$tgico-recent: "\e98d";
|
||||
$tgico-replace: "\e98e";
|
||||
$tgico-reply: "\e98f";
|
||||
$tgico-reply_filled: "\e990";
|
||||
$tgico-rightpanel: "\e991";
|
||||
$tgico-rotate_left: "\e992";
|
||||
$tgico-rotate_right: "\e993";
|
||||
$tgico-saved: "\e994";
|
||||
$tgico-savedmessages: "\e995";
|
||||
$tgico-schedule: "\e996";
|
||||
$tgico-scheduled: "\e997";
|
||||
$tgico-search: "\e998";
|
||||
$tgico-select: "\e999";
|
||||
$tgico-send: "\e99a";
|
||||
$tgico-send2: "\e99b";
|
||||
$tgico-sending: "\e99c";
|
||||
$tgico-sendingerror: "\e99d";
|
||||
$tgico-settings: "\e99e";
|
||||
$tgico-settings_filled: "\e99f";
|
||||
$tgico-sharescreen_filled: "\e9a0";
|
||||
$tgico-shipping: "\e9a1";
|
||||
$tgico-shuffle: "\e9a2";
|
||||
$tgico-smallscreen: "\e9a3";
|
||||
$tgico-smile: "\e9a4";
|
||||
$tgico-spoiler: "\e9a5";
|
||||
$tgico-sport: "\e9a6";
|
||||
$tgico-stickers: "\e9a7";
|
||||
$tgico-stop: "\e9a8";
|
||||
$tgico-strikethrough: "\e9a9";
|
||||
$tgico-textedit: "\e9aa";
|
||||
$tgico-tip: "\e9ab";
|
||||
$tgico-tools: "\e9ac";
|
||||
$tgico-unarchive: "\e9ad";
|
||||
$tgico-underline: "\e9ae";
|
||||
$tgico-unmute: "\e9af";
|
||||
$tgico-unpin: "\e9b0";
|
||||
$tgico-unread: "\e9b1";
|
||||
$tgico-up: "\e9b2";
|
||||
$tgico-user: "\e9b3";
|
||||
$tgico-username: "\e9b4";
|
||||
$tgico-videocamera: "\e9b5";
|
||||
$tgico-videocamera_crossed_filled: "\e9b6";
|
||||
$tgico-videocamera_filled: "\e9b7";
|
||||
$tgico-videochat: "\e9b8";
|
||||
$tgico-volume_down: "\e9b9";
|
||||
$tgico-volume_mute: "\e9ba";
|
||||
$tgico-volume_off: "\e9bb";
|
||||
$tgico-volume_up: "\e9bc";
|
||||
$tgico-zoomin: "\e9bd";
|
||||
$tgico-zoomout: "\e9be";
|
||||
|
||||
|
59
src/tests/cards.test.ts
Normal file
59
src/tests/cards.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import cardFormattingPatterns from "../helpers/cards/cardFormattingPatterns";
|
||||
import formatValueByPattern from "../helpers/cards/formatValueByPattern";
|
||||
import { validateCardExpiry, validateCardNumber } from "../helpers/cards/validateCard";
|
||||
|
||||
describe('Card number', () => {
|
||||
test('Format', () => {
|
||||
const data = [
|
||||
['4242424242424242', '4242 4242 4242 4242'],
|
||||
['371758885524003', '3717 588855 24003']
|
||||
];
|
||||
|
||||
data.forEach(([plain, formatted]) => {
|
||||
const result = formatValueByPattern(cardFormattingPatterns.cardNumber, plain);
|
||||
expect(result.value).toEqual(formatted);
|
||||
});
|
||||
});
|
||||
|
||||
test('Validate', () => {
|
||||
const data = [
|
||||
['4242424242424242', null],
|
||||
['4242424242424241', 'invalid'],
|
||||
['424242424242424', 'incomplete']
|
||||
];
|
||||
|
||||
data.forEach(([cardNumber, code]) => {
|
||||
const result = validateCardNumber(cardNumber);
|
||||
if(code) {
|
||||
expect(result.code).toEqual(code);
|
||||
} else {
|
||||
expect(result).toEqual(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Expiry date', () => {
|
||||
const joiner = '/';
|
||||
const getExpiryDate = (date: Date) => `${date.getMonth()}${joiner}${date.getFullYear() % 100}`;
|
||||
|
||||
test('Format', () => {
|
||||
const month = 10;
|
||||
const year = 20;
|
||||
|
||||
const {value} = formatValueByPattern(cardFormattingPatterns.cardExpiry, `${month}${year}`);
|
||||
expect(value).toEqual(`${month}${joiner}${year}`);
|
||||
});
|
||||
|
||||
test('Expired', () => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
expect(validateCardExpiry(getExpiryDate(date))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Nonexpired', () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() + 1);
|
||||
expect(validateCardExpiry(getExpiryDate(date))).toEqual(null);
|
||||
});
|
||||
});
|
25
src/types.d.ts
vendored
25
src/types.d.ts
vendored
@ -120,3 +120,28 @@ export type SendMessageEmojiInteractionData = {
|
||||
a: {t: number, i: 1}[],
|
||||
v: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* @link https://core.telegram.org/api/web-events#postmessage-api
|
||||
*/
|
||||
export type TelegramWebviewEventMap = {
|
||||
payment_form_submit: {
|
||||
credentials: any,
|
||||
title: string
|
||||
},
|
||||
web_app_open_tg_link: {
|
||||
path_full: string // '/username'
|
||||
}
|
||||
};
|
||||
|
||||
export type TelegramWebviewSerializedEvent<T extends keyof TelegramWebviewEventMap> = {
|
||||
eventType: T,
|
||||
eventData: TelegramWebviewEventMap[T]
|
||||
};
|
||||
|
||||
export type TelegramWebviewSerializedEvents = {
|
||||
[type in keyof TelegramWebviewEventMap]: TelegramWebviewSerializedEvent<type>
|
||||
};
|
||||
|
||||
export type TelegramWebviewEvent = TelegramWebviewSerializedEvents[keyof TelegramWebviewEventMap];
|
||||
export type TelegramWebviewEventCallback = (event: TelegramWebviewEvent) => void;
|
||||
|
Loading…
x
Reference in New Issue
Block a user