nwa-client
Version:
Native WebApp client library
439 lines (409 loc) • 17.1 kB
text/typescript
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AppRequest } from './models';
import { IApp } from './interfaces';
declare global {
interface Window {
App: IApp | undefined;
NwaHandler: any;
NwaConsole: any;
select: unknown;
}
interface XMLHttpRequest {
_url: string | URL;
}
interface Navigator {
userAgentData?: any;
}
}
function generateDeviceId() {
const deviceId = self.crypto.randomUUID();
localStorage.setItem('deviceId', deviceId);
return deviceId;
}
function hasMinVersion(version) {
const matchApp = /(\d+)\.(\d+)\.(\d+)\+(\d+) NWA/i.exec(navigator.userAgent);
const matchMin = /(\d+)\.(\d+)\.(\d+)/i.exec(version);
if (!matchApp || !matchMin) {
return false;
}
const [majorA, minorA, patchA] = matchApp.slice(1).map(Number);
const [majorB, minorB, patchB] = matchMin.slice(1).map(Number);
if (majorA !== majorB) {
return majorA > majorB;
}
if (minorA !== minorB) {
return minorA > minorB;
}
return patchA >= patchB;
}
function getMeta(name) {
const meta = document.querySelector('meta[name="' + name + '"]');
if (meta) {
return meta.getAttribute('content');
} else {
return '#FFFFFFFF';
}
}
function getInfos() {
const nVer = navigator.appVersion;
const nAgt = navigator.userAgent;
let browserName = navigator.appName;
let fullVersion = '' + parseFloat(navigator.appVersion);
let majorVersion = parseInt(navigator.appVersion, 10);
let nameOffset, verOffset, ix;
// In Opera, the true version is after "Opera" or after "Version"
if ((verOffset = nAgt.indexOf('Opera')) !== -1) {
browserName = 'Opera';
fullVersion = nAgt.substring(verOffset + 6);
if ((verOffset = nAgt.indexOf('Version')) !== -1) {
fullVersion = nAgt.substring(verOffset + 8);
}
}
// In MSIE, the true version is after "MSIE" in userAgent
else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) {
browserName = 'Microsoft Internet Explorer';
fullVersion = nAgt.substring(verOffset + 5);
}
// In Chrome, the true version is after "Chrome"
else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) {
browserName = 'Chrome';
fullVersion = nAgt.substring(verOffset + 7);
}
// In Safari, the true version is after "Safari" or after "Version"
else if ((verOffset = nAgt.indexOf('Safari')) !== -1) {
browserName = 'Safari';
fullVersion = nAgt.substring(verOffset + 7);
if ((verOffset = nAgt.indexOf('Version')) !== -1) {
fullVersion = nAgt.substring(verOffset + 8);
}
}
// In Firefox, the true version is after "Firefox"
else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) {
browserName = 'Firefox';
fullVersion = nAgt.substring(verOffset + 8);
}
// In most other browsers, "name/version" is at the end of userAgent
else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) {
browserName = nAgt.substring(nameOffset, verOffset);
fullVersion = nAgt.substring(verOffset + 1);
if (browserName.toLowerCase() === browserName.toUpperCase()) {
browserName = navigator.appName;
}
}
// trim the fullVersion string at semicolon/space if present
if ((ix = fullVersion.indexOf(';')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
if ((ix = fullVersion.indexOf(' ')) !== -1) {
fullVersion = fullVersion.substring(0, ix);
}
majorVersion = parseInt('' + fullVersion, 10);
if (isNaN(majorVersion)) {
fullVersion = '' + parseFloat(navigator.appVersion);
majorVersion = parseInt(navigator.appVersion, 10);
}
let platform = navigator.userAgentData?.platform?.toLowerCase() || navigator.platform.toLowerCase();
if (/iphone|ipad|ipod/.test(platform)) {
platform = 'ios';
} else if (/android/.test(platform)) {
platform = 'android';
} else if (/win/.test(platform)) {
platform = 'windows';
} else if (/mac/.test(platform)) {
platform = 'macos';
} else if (/linux/.test(platform)) {
platform = 'linux';
}
return {
browserName,
fullVersion,
majorVersion,
platform,
};
}
function logToString(data) {
return data.map(item => typeof item === 'object' && item !== null ? JSON.stringify(item) : String(item)).join(' ');
}
let App: any;
const isNativeWebApp = /NWA/i.test(navigator.userAgent);
const isUpToDate = hasMinVersion('2.6.0');
if (isNativeWebApp && isUpToDate && window.NwaHandler) {
const isAndroid = /android/i.test(navigator.userAgent);
let requestId = 0;
const handlers: ((...args) => boolean | void)[][] = [];
const requests: AppRequest[] = [];
App = new Proxy(window.NwaHandler, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target: any, propKey: string) {
if (propKey === 'web' || propKey === 'isWeb') {
return false;
}
if (propKey === 'isMobile' || propKey === 'mobile' || propKey === 'nwa' || propKey === 'isNwa') {
return true;
}
return (...args) => {
if (propKey === 'notify' || propKey === 'emit') {
const channel = args.shift();
if (channel in handlers) {
try {
let success = true;
for (const handler of handlers[channel]) {
success = success && handler(...args) !== false;
}
return success;
} catch (e) {
return false;
}
} else {
return false;
}
} else if (propKey === 'resolve' || propKey === 'reject' || propKey === 'onSuccess' || propKey === 'onError') {
const request = requests[args[0]];
if (request) {
if (args.length > 1) {
return request[propKey](args[1]);
} else {
return request[propKey]();
}
} else {
// eslint-disable-next-line max-len, no-console
console.error('Unable to resolve request. (id=' + args[0] + ', now=' + Date.now() + ', data=' + JSON.stringify(args[1]) + ')');
}
} else if (target[propKey] === undefined) {
if (propKey === 'on') {
if (!handlers[args[0]]) {
handlers[args[0]] = [];
}
handlers[args[0]].push(args[1]);
}
const id = 'req-' + (requestId++) + '-' + Date.now();
if (isAndroid) {
return new Promise((resolve, reject) => {
requests[id] = { resolve, reject };
target.postMessage(propKey, id, JSON.stringify(args));
});
} else {
return target.postMessage(JSON.stringify({ method: propKey, params: args })).then((result) => {
if (result && result.compatError) {
throw result.compatError;
} else {
return result;
}
});
}
}
};
},
});
// Register callbacks
window.NwaHandler.resolve = App.resolve;
window.NwaHandler.reject = App.reject;
window.NwaHandler.notify = App.notify;
window.NwaHandler.emit = App.notify; // Compat, to remove
window.NwaHandler.onNotification = App.notify; // Compat, to remove
// Bypass native browser functionalities
if (isAndroid) {
window.print = () => App.print();
navigator.share = (data) => App.share(data);
navigator.clipboard.writeText = (data) => App.copy(data);
navigator.clipboard.readText = () => App.paste();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigator.setAppBadge = (data) => App.setAppBadge(data);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigator.clearAppBadge = () => App.clearAppBadge();
navigator.vibrate = (data) => App.vibrate(data);
window.open = (url, name) => App.openUrl(url, name);
if (!('permissions' in navigator)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigator.permissions = {};
}
navigator.permissions.query = App.hasPermission;
if (window.NwaConsole) {
console.log = (...data) => window.NwaConsole.log('log', logToString(data));
console.debug = (...data) => window.NwaConsole.log('debug', logToString(data));
console.info = (...data) => window.NwaConsole.log('info', logToString(data));
console.warn = (...data) => window.NwaConsole.log('warning', '⚠️ ' + logToString(data));
console.error = (...data) => window.NwaConsole.log('error', '❌ ' + logToString(data));
}
} else {
const positionCallbacks: any[] = [];
window.print = () => App.print();
window.open = (url, name) => App.openUrl(url, name);
navigator.share = (data) => App.share(data);
navigator.clipboard.writeText = (data) => App.copy(data);
navigator.clipboard.readText = () => App.paste();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigator.setAppBadge = (data) => App.setAppBadge(data);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
navigator.clearAppBadge = () => App.clearAppBadge();
navigator.vibrate = (data) => App.vibrate(data);
App.on('position', location => {
if (positionCallbacks.length) {
positionCallbacks.forEach(obj => obj.callback(location));
} else {
App.watchPosition(false);
}
});
navigator.geolocation.watchPosition = (callback, errorCallback, options) => {
const watchId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
if (!positionCallbacks.length) {
App.watchPosition(true, options).then(callback).catch(errorCallback);
}
positionCallbacks.push({
id: watchId,
callback,
});
return watchId;
};
navigator.geolocation.clearWatch = (watchId) => {
positionCallbacks.filter((cb, i, arr) => {
if (cb.id === watchId) {
arr.splice(i, 1);
return true;
} else {
return false;
}
});
if (!positionCallbacks.length) {
App.watchPosition(false);
}
};
navigator.geolocation.getCurrentPosition = (callback, errorCallback, settings) => {
return App.getCurrentPosition(settings).then(callback).catch(errorCallback);
};
if (window.NwaConsole) {
console.log = (...data) => window.NwaConsole.postMessage({ level: 'log', message: logToString(data) });
console.debug = (...data) => window.NwaConsole.postMessage({ level: 'debug', message: logToString(data) });
console.info = (...data) => window.NwaConsole.postMessage({ level: 'info', message: logToString(data) });
console.warn = (...data) => window.NwaConsole.postMessage({ level: 'warning', message: '⚠️ ' + logToString(data) });
console.error = (...data) => window.NwaConsole.postMessage({ level: 'error', message: '❌ ' + logToString(data) });
}
/*
(function () {
// XMLHttpRequest
const open = XMLHttpRequest.prototype.open;
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method: string, url: string | URL) {
this._url = url;
return open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
this.addEventListener('load', function () {
console.log('XHR success:', this._url);
});
this.addEventListener('error', function () {
console.error('XHR failed:', this._url);
});
return send.apply(this, arguments);
};
// Fetch
const fetchOrig = window.fetch;
window.fetch = function (resource, init) {
return fetchOrig(resource, init)
.then(response => {
//console.log("✅ Fetch:", resource, response.status);
return response;
})
.catch(error => {
console.error('Fetch failed:', resource, error.message);
throw error;
});
};
// Image.src
const imgDesc = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
if (imgDesc) {
Object.defineProperty(Image.prototype, 'src', {
set: function (value) {
//console.log("🖼️ Image chargée :", value);
return imgDesc.set.call(this, value);
},
get: function () {
return imgDesc.get.call(this);
},
});
}
})();
*/
}
window.select = function (params: any) {
return App.select(params).then((files: any) => {
const dataTransfer = new DataTransfer();
for (const file of files) {
const bytes = atob(file.data);
let length = bytes.length;
const out = new Uint8Array(length);
// Loop and convert.
while (length--) {
out[length] = bytes.charCodeAt(length);
}
dataTransfer.items.add(new File([out], file.name, { type: file.type }));
}
return dataTransfer;
});
};
document.addEventListener('DOMContentLoaded', () => {
try {
const viewport = getMeta('viewport');
const keyboardResize = !!viewport && viewport.includes('interactive-widget=resizes-content');
const appBarOffset = !!viewport && !viewport.includes('viewport-fit=cover');
const navBarOffset = !!viewport && !viewport.includes('viewport-fit=cover');
const backgroundColor = parseInt(getMeta('theme-color')!.substring(1), 16);
const appBarColor = parseInt(getMeta('app-bar-color')!.substring(1), 16);
const navBarColor = parseInt(getMeta('nav-bar-color')!.substring(1), 16);
App.updateAppStyle({
backgroundColor,
appBarColor,
navBarColor,
appBarOffset,
navBarOffset,
keyboardResize,
});
} catch (e) {
console.error('Unable to update app style', e);
}
}, false);
App.getDeviceId().then((deviceId) => localStorage.setItem('deviceId', deviceId));
} else {
if (isNativeWebApp) {
if (!isUpToDate) {
console.error('Deprecated NWA version');
}
if (!window.NwaHandler) {
console.error('NwaHandler not registered');
}
}
const deviceId = localStorage.getItem('deviceId') ?? generateDeviceId();
App = {
web: true,
isWeb: true,
mobile: false,
isMobile: false,
nwa: false,
isNwa: false,
getDeviceInfos: async () => {
const infos = getInfos();
return {
id: deviceId,
type: 'browser',
os: infos.platform,
version: infos.fullVersion,
model: infos.browserName,
name: infos.browserName,
appVersion: null,
registrationToken: null,
};
},
getDeviceId: async () => deviceId,
getReport: async () => null,
on: async (event, handler) => {
//
},
};
}
export default App;