|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import type { DcId } from '../types';
|
|
|
|
import type { ApiError } from '../lib/mtproto/apiManager';
|
|
|
|
import apiManager from '../lib/mtproto/mtprotoworker';
|
|
|
|
import Page from './page';
|
|
|
|
import serverTimeManager from '../lib/mtproto/serverTimeManager';
|
|
|
|
import { AuthAuthorization, AuthLoginToken } from '../layer';
|
|
|
|
import { bytesCmp, bytesToBase64 } from '../helpers/bytes';
|
|
|
|
import App from '../config/app';
|
|
|
|
import Button from '../components/button';
|
|
|
|
import { _i18n, i18n, LangPackKey } from '../lib/langPack';
|
|
|
|
import appStateManager from '../lib/appManagers/appStateManager';
|
|
|
|
import rootScope from '../lib/rootScope';
|
|
|
|
import { putPreloader } from '../components/misc';
|
|
|
|
import getLanguageChangeButton from '../components/languageChangeButton';
|
|
|
|
import { pause } from '../helpers/schedulers/pause';
|
|
|
|
|
|
|
|
const FETCH_INTERVAL = 3;
|
|
|
|
|
|
|
|
let onFirstMount = async() => {
|
|
|
|
const pageElement = page.pageEl;
|
|
|
|
const imageDiv = pageElement.querySelector('.auth-image') as HTMLDivElement;
|
|
|
|
|
|
|
|
let preloader = putPreloader(imageDiv, true);
|
|
|
|
|
|
|
|
const inputWrapper = document.createElement('div');
|
|
|
|
inputWrapper.classList.add('input-wrapper');
|
|
|
|
|
|
|
|
const btnBack = Button('btn-primary btn-secondary btn-primary-transparent primary', {text: 'Login.QR.Cancel'});
|
|
|
|
inputWrapper.append(btnBack);
|
|
|
|
|
|
|
|
getLanguageChangeButton(inputWrapper);
|
|
|
|
|
|
|
|
const container = imageDiv.parentElement;
|
|
|
|
|
|
|
|
const h4 = document.createElement('h4');
|
|
|
|
_i18n(h4, 'Login.QR.Title');
|
|
|
|
|
|
|
|
const helpList = document.createElement('ol');
|
|
|
|
helpList.classList.add('qr-description');
|
|
|
|
(['Login.QR.Help1', 'Login.QR.Help2', 'Login.QR.Help3'] as LangPackKey[]).forEach((key) => {
|
|
|
|
const li = document.createElement('li');
|
|
|
|
li.append(i18n(key));
|
|
|
|
helpList.append(li);
|
|
|
|
});
|
|
|
|
|
|
|
|
container.append(h4, helpList, inputWrapper);
|
|
|
|
|
|
|
|
btnBack.addEventListener('click', () => {
|
|
|
|
import('./pageSignIn').then(m => m.default.mount());
|
|
|
|
stop = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
const results = await Promise.all([
|
|
|
|
import('qr-code-styling' as any)
|
|
|
|
]);
|
|
|
|
const QRCodeStyling = results[0].default;
|
|
|
|
|
|
|
|
let stop = false;
|
|
|
|
rootScope.addEventListener('user_auth', () => {
|
|
|
|
stop = true;
|
|
|
|
cachedPromise = null;
|
|
|
|
}, {once: true});
|
|
|
|
|
|
|
|
let options: {dcId?: DcId, ignoreErrors: true} = {ignoreErrors: true};
|
|
|
|
let prevToken: Uint8Array | number[];
|
|
|
|
|
|
|
|
const iterate = async(isLoop: boolean) => {
|
|
|
|
try {
|
|
|
|
let loginToken = await apiManager.invokeApi('auth.exportLoginToken', {
|
|
|
|
api_id: App.id,
|
|
|
|
api_hash: App.hash,
|
|
|
|
except_ids: []
|
|
|
|
}, {ignoreErrors: true});
|
|
|
|
|
|
|
|
if(loginToken._ === 'auth.loginTokenMigrateTo') {
|
|
|
|
if(!options.dcId) {
|
|
|
|
options.dcId = loginToken.dc_id as DcId;
|
|
|
|
apiManager.setBaseDcId(loginToken.dc_id);
|
|
|
|
//continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
loginToken = await apiManager.invokeApi('auth.importLoginToken', {
|
|
|
|
token: loginToken.token
|
|
|
|
}, options) as AuthLoginToken.authLoginToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(loginToken._ === 'auth.loginTokenSuccess') {
|
|
|
|
const authorization = loginToken.authorization as any as AuthAuthorization.authAuthorization;
|
|
|
|
apiManager.setUserAuth(authorization.user.id);
|
|
|
|
import('./pageIm').then(m => m.default.mount());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* // to base64
|
|
|
|
var decoder = new TextDecoder('utf8');
|
|
|
|
var b64encoded = btoa(String.fromCharCode.apply(null, [...loginToken.token])); */
|
|
|
|
|
|
|
|
if(!prevToken || !bytesCmp(prevToken, loginToken.token)) {
|
|
|
|
prevToken = loginToken.token;
|
|
|
|
|
|
|
|
let encoded = bytesToBase64(loginToken.token);
|
|
|
|
let url = "tg://login?token=" + encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, "");
|
|
|
|
|
|
|
|
const style = window.getComputedStyle(document.documentElement);
|
|
|
|
const surfaceColor = style.getPropertyValue('--surface-color').trim();
|
|
|
|
const textColor = style.getPropertyValue('--primary-text-color').trim();
|
|
|
|
const primaryColor = style.getPropertyValue('--primary-color').trim();
|
|
|
|
|
|
|
|
const logoUrl = await fetch('assets/img/logo_padded.svg')
|
|
|
|
.then(res => res.text())
|
|
|
|
.then(text => {
|
|
|
|
text = text.replace(/(fill:).+?(;)/, `$1${primaryColor}$2`);
|
|
|
|
const blob = new Blob([text], {type: 'image/svg+xml;charset=utf-8'});
|
|
|
|
|
|
|
|
// * because iOS Safari doesn't want to eat objectURL
|
|
|
|
return new Promise<string>((resolve) => {
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (e) => {
|
|
|
|
resolve(e.target.result as string);
|
|
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
|
|
});
|
|
|
|
//return URL.createObjectURL(blob);
|
|
|
|
});
|
|
|
|
|
|
|
|
const qrCode = new QRCodeStyling({
|
|
|
|
width: 240 * window.devicePixelRatio,
|
|
|
|
height: 240 * window.devicePixelRatio,
|
|
|
|
data: url,
|
|
|
|
image: logoUrl,
|
|
|
|
dotsOptions: {
|
|
|
|
color: textColor,
|
|
|
|
type: 'rounded'
|
|
|
|
},
|
|
|
|
cornersSquareOptions: {
|
|
|
|
type: 'extra-rounded'
|
|
|
|
},
|
|
|
|
imageOptions: {
|
|
|
|
imageSize: 1,
|
|
|
|
margin: 0
|
|
|
|
},
|
|
|
|
backgroundOptions: {
|
|
|
|
color: surfaceColor
|
|
|
|
},
|
|
|
|
qrOptions: {
|
|
|
|
errorCorrectionLevel: "L"
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
qrCode.append(imageDiv);
|
|
|
|
(imageDiv.lastChild as HTMLCanvasElement).classList.add('qr-canvas');
|
|
|
|
|
|
|
|
let promise: Promise<void>;
|
|
|
|
if(qrCode._drawingPromise) {
|
|
|
|
promise = qrCode._drawingPromise;
|
|
|
|
} else {
|
|
|
|
promise = Promise.race([
|
|
|
|
pause(1000),
|
|
|
|
new Promise<void>((resolve) => {
|
|
|
|
qrCode._canvas._image.addEventListener('load', () => {
|
|
|
|
window.requestAnimationFrame(() => resolve());
|
|
|
|
}, {once: true});
|
|
|
|
})
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// * это костыль, но библиотека не предоставляет никаких событий
|
|
|
|
await promise.then(() => {
|
|
|
|
if(preloader) {
|
|
|
|
preloader.style.animation = 'hide-icon .4s forwards';
|
|
|
|
|
|
|
|
const c = imageDiv.children[1] as HTMLElement;
|
|
|
|
c.style.display = 'none';
|
|
|
|
c.style.animation = 'grow-icon .4s forwards';
|
|
|
|
setTimeout(() => {
|
|
|
|
c.style.display = '';
|
|
|
|
}, 150);
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
c.style.animation = '';
|
|
|
|
}, 500);
|
|
|
|
preloader = undefined;
|
|
|
|
} else {
|
|
|
|
Array.from(imageDiv.children).slice(0, -1).forEach(el => {
|
|
|
|
el.remove();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isLoop) {
|
|
|
|
let timestamp = Date.now() / 1000;
|
|
|
|
let diff = loginToken.expires - timestamp - serverTimeManager.serverTimeOffset;
|
|
|
|
|
|
|
|
await pause(diff > FETCH_INTERVAL ? 1e3 * FETCH_INTERVAL : 1e3 * diff | 0);
|
|
|
|
}
|
|
|
|
} catch(err) {
|
|
|
|
switch((err as ApiError).type) {
|
|
|
|
case 'SESSION_PASSWORD_NEEDED':
|
|
|
|
console.warn('pageSignQR: SESSION_PASSWORD_NEEDED');
|
|
|
|
(err as ApiError).handled = true;
|
|
|
|
import('./pagePassword').then(m => m.default.mount());
|
|
|
|
stop = true;
|
|
|
|
cachedPromise = null;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.error('pageSignQR: default error:', err);
|
|
|
|
stop = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
//await iterate(false);
|
|
|
|
|
|
|
|
return async() => {
|
|
|
|
stop = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
if(stop) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const needBreak = await iterate(true);
|
|
|
|
if(needBreak) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} while(true);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
let cachedPromise: Promise<() => Promise<void>>;
|
|
|
|
const page = new Page('page-signQR', true, () => {
|
|
|
|
return cachedPromise;
|
|
|
|
}, () => {
|
|
|
|
//console.log('onMount');
|
|
|
|
if(!cachedPromise) cachedPromise = onFirstMount();
|
|
|
|
cachedPromise.then(func => {
|
|
|
|
func();
|
|
|
|
});
|
|
|
|
|
|
|
|
appStateManager.pushToState('authState', {_: 'authStateSignQr'});
|
|
|
|
});
|
|
|
|
|
|
|
|
export default page;
|