Skip to content

Commit

Permalink
Merge pull request #175 from e-picsa/feat/device-support
Browse files Browse the repository at this point in the history
Feat/device support
  • Loading branch information
chrismclarke authored Sep 18, 2023
2 parents ad38428 + 05d7a34 commit b52f5dd
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 63 deletions.
1 change: 0 additions & 1 deletion apps/picsa-apps/extension-app/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<!-- <div>Extenstion Toolkit</div> -->
<div class="page">
<picsa-header></picsa-header>
<router-outlet></router-outlet>
Expand Down
23 changes: 12 additions & 11 deletions apps/picsa-apps/extension-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable @nrwl/nx/enforce-module-boundaries */
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Capacitor } from '@capacitor/core';
import { ENVIRONMENT } from '@picsa/environments';
import { AnalyticsService } from '@picsa/shared/services/core/analytics.service';
import { CrashlyticsService } from '@picsa/shared/services/core/crashlytics.service';
import { PerformanceService } from '@picsa/shared/services/core/performance.service';
import { CrashlyticsService } from '@picsa/shared/services/native/crashlytics.service';

@Component({
selector: 'picsa-root',
Expand All @@ -16,17 +15,19 @@ export class AppComponent {
title = 'extension-toolkit';

constructor(
analyticsService: AnalyticsService,
router: Router,
crashlyticsService: CrashlyticsService,
performanceService: PerformanceService
private analyticsService: AnalyticsService,
private router: Router,
private performanceService: PerformanceService,
private crashlyticsService: CrashlyticsService
) {
performanceService.setEnabled({ enabled: ENVIRONMENT.production });
this.init();
}

private async init() {
this.performanceService.setEnabled({ enabled: ENVIRONMENT.production });
this.crashlyticsService.ready().then(() => null);
if (ENVIRONMENT.production) {
analyticsService.init(router);
}
if (Capacitor.isNativePlatform()) {
crashlyticsService.init();
this.analyticsService.init(this.router);
}
}
}
190 changes: 190 additions & 0 deletions apps/picsa-apps/extension-app/src/assets/compatibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/********************************************************************************
* Script to ensure user has up-to-date version of google chrome for Android
* Should be included in main `index.html` via script tag, e.g.
*
* <script
type="text/javascript"
id="compatibility"
src="/assets/compatibility.js"
async="false"
defer="false"
></script>
*******************************************************************************/

/** Min android version as mapped from `minSdkVersion` (API 23, Android 6.0) */
const minAndroidVersion = 6.0;

/**
* Minimum version of chrome required to run app
* Min baseline 45 to support arrow functions: https://caniuse.com/arrow-functions
* Capacitor requirement 60, according to: https://capacitorjs.com/docs/android
* Codebase pdf viewer 92 (https://caniuse.com/mdn-javascript_builtins_string_at)
* Legacy chrome versions available at: https://www.chromium.org/getting-involved/download-chromium/
*
* NOTE - whilst capacitor does have functionality to detect version and present custom error page,
* using the `capacitor.config.ts` android property `minWebViewVersion`, however this requires capacitor
* to load correctly which often does not happen
*/
const minAndroidWebviewVersion = 93;

/**
* Check for compatibiliy issues that may arise before main app loads
* This script is designed to be called from main index.html file to detect
*
* Uses web navigator instead of native APIs as Capacitor will fail to initialise
* in some legacy browsers
*/
function checkCompatibility() {
const info = getInfo();
if (info.operatingSystem === 'android') {
// Catch case where app may be sideloaded onto a device with sdk lower than `minSdkVersion` (API 23, Android 6.0)
// Additionally the render prompt update will fail due to use of template literals
if (info.androidVersion && info.androidVersion < minAndroidVersion) {
alert('This app is not supported on Android 5.\nPlease use a device running Android 6 or higher');
return;
}
// Check chrome webview version up-to-date
if (info.chromeVersion && info.chromeVersion < minAndroidWebviewVersion) {
console.log('[Compatibility check]');
console.log(JSON.stringify(info, null, 2));
// Webview version is controlled by different apps depending on android version
// For android 7-9 this is the controlled by the preinstalled Google Chrome app
// For android 6 and 10+ this is controlled by the standalone Android Webview app
// https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/faq.md#what_s-the-relationship-between-webview-and-chrome
// https://techblogs.42gears.com/webkit-provider-changes-in-various-android-versions/

if (info.androidVersion >= 7 && info.androidVersion <= 9) {
renderUpdatePrompt('Google Chrome', 'https://play.google.com/store/apps/details?id=com.android.chrome');
} else {
renderUpdatePrompt(
'Android Webview',
'https://play.google.com/store/apps/details?id=com.google.android.webview'
);
}
}
}
}

checkCompatibility();

/**
* Attempt to get core device info by parsing the navigator user object
* Adapted from Capacitor Device api methods to support case where Capacitor itself
* fails to correctly initialise (e.g. some legacy browsers)
* https://github.com/ionic-team/capacitor-plugins/blob/main/device/src/web.ts
* @returns
*/
function getInfo() {
const uaFields = {};
const ua = navigator.userAgent;
const start = ua.indexOf('(') + 1;
const end = ua.indexOf(') AppleWebKit');
const fields = ua.substring(start, end);
if (ua.indexOf('Android') !== -1) {
const tmpFields = fields.replace('; wv', '').split('; ').pop();
if (tmpFields) {
uaFields.model = tmpFields.split(' Build')[0];
}
uaFields.osVersion = fields.split('; ')[1];
}
if (/android/i.test(ua)) {
uaFields.operatingSystem = 'android';
}
// Additional fields that would normally be determined using native code (adapted for web)
if (uaFields.operatingSystem === 'android') {
uaFields.androidVersion = parseFloat(uaFields.osVersion.toLowerCase().replace('android', ''));
uaFields.chromeVersion = getChromeVersion();
}
return uaFields;
}

function getChromeVersion() {
const ua = navigator.userAgent.toLowerCase();
const regex = /chrome\/([0-9]*)\./;
const res = regex.exec(ua);
if (res) {
const chromeVersion = parseInt(res[1]);
return chromeVersion;
}
}

/** Create a custom popup element that blocks the screen to force user to update before continuing */
function renderUpdatePrompt(appName, appLink) {
const backdropEl = document.createElement('div');
backdropEl.id = 'updatePrompt';
const styles = `
position:absolute;
top:0;
left:0;
height:100vh;
width:100vw;
z-index:2;
background:#000c;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center
`;
backdropEl.style.cssText = styles;

// Main content container
const contentEl = document.createElement('div');
contentEl.style.cssText = `
width:300px;
background:#e9e9e9;
padding: 16px;
border-radius: 8px;
`;

// Close button
const closeButtonEl = document.createElement('button');
closeButtonEl.style.cssText = `
float:right;
`;
closeButtonEl.textContent = 'X';
closeButtonEl.onclick = closePrompt;
contentEl.appendChild(closeButtonEl);

// Heading
const headingEl = document.createElement('h2');
(headingEl.textContent = 'Update Required'), (headingEl.style.cssText = `text-align:center`);
contentEl.appendChild(headingEl);

// Text
const textEl = document.createElement('p');
textEl.innerHTML = `Please update the <u>${appName}</u> app from the play store and restart the app`;
contentEl.appendChild(textEl);

// Action button
const buttonEl = document.createElement('button');
buttonEl.textContent = 'Go To Play Store';
buttonEl.style.cssText = `
width: 100%;
height: 48px;
margin: 16px 0;
font-size: 16px;
padding: 8px;
background: #01875f;
color: white;
border-radius: 8px;
font-weight: bold;
border: none;
cursor: pointer;
`;

// Action button link
const linkEl = document.createElement('a');
linkEl.href = appLink;
linkEl.target = '_blank';
linkEl.appendChild(buttonEl);
contentEl.appendChild(linkEl);

// Append to main content
backdropEl.appendChild(contentEl);
const bodyEl = document.querySelector('body');
bodyEl.appendChild(backdropEl);
}

function closePrompt() {
document.getElementById('updatePrompt').remove();
}
7 changes: 7 additions & 0 deletions apps/picsa-apps/extension-app/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<script
type="text/javascript"
id="compatibility"
src="/assets/compatibility.js"
async="false"
defer="false"
></script>
<picsa-root></picsa-root>
</body>
</html>
64 changes: 64 additions & 0 deletions libs/shared/src/services/core/crashlytics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { Device } from '@capacitor/device';
import { FirebaseCrashlytics, RecordExceptionOptions } from '@capacitor-community/firebase-crashlytics';
import { ENVIRONMENT } from '@picsa/environments';

import { PicsaAsyncService } from '../asyncService.service';

@Injectable({
providedIn: 'root',
})
/**
* Automates reporting of crash data to firebase crashlytics, and adds methods
* to allow custom reporting for non-fatal exceptions (e.g. error messages)
* https://github.com/capacitor-community/firebase-crashlytics
*/
export class CrashlyticsService extends PicsaAsyncService {
/** Service will only be enabled in production on native device (not supported on web) */
private enabled = false;

constructor() {
super();
this.enabled = Capacitor.isNativePlatform() && ENVIRONMENT.production;
}
public override async init() {
if (this.enabled) {
const { setEnabled, setUserId, setContext, sendUnsentReports } = FirebaseCrashlytics;
await setEnabled({ enabled: true });
const { uuid } = await Device.getId();
await setUserId({ userId: uuid });
// populate webview useragent info
const { webViewVersion } = await Device.getInfo();
await setContext({
key: 'userAgent',
type: 'string',
value: navigator.userAgent || '',
});
await setContext({
key: 'webViewVersion',
type: 'string',
value: webViewVersion || '',
});
await setContext({
key: 'pathname',
type: 'string',
value: location.pathname || '',
});
sendUnsentReports();
} else {
this.loadDummyMethods();
}
}

public recordException = FirebaseCrashlytics.recordException;

/** When using on unsupported device fill dummy methods for interoperability */
private loadDummyMethods() {
this.recordException = async (options: RecordExceptionOptions) => {
console.warn('[Crashlytics] skipping report', options);
};
}

private crash = FirebaseCrashlytics.crash;
}
24 changes: 10 additions & 14 deletions libs/shared/src/services/core/error-handler.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { ErrorHandler, Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { fromError as getStacktraceFromError } from 'stacktrace-js';

import { CrashlyticsService } from '../native/crashlytics.service';
import { CrashlyticsService } from './crashlytics.service';

@Injectable({
providedIn: 'root',
})
export class ErrorHandlerService extends ErrorHandler {
// Error handling is important and needs to be loaded first.
// Because of this we should manually inject the services with Injector.
constructor(private injector: Injector) {
constructor(private crashlyticsService: CrashlyticsService) {
super();
}

Expand All @@ -19,8 +17,9 @@ export class ErrorHandlerService extends ErrorHandler {
* (console logs and modal in dev mode, ignored in production), on android
* this logs to firebase crashlytics
*/
override handleError(error: Error) {
override async handleError(error: Error) {
if (Capacitor.isNativePlatform()) {
await this.crashlyticsService.ready();
return this.logToCrashlytics(error);
} else {
super.handleError(error);
Expand All @@ -29,13 +28,10 @@ export class ErrorHandlerService extends ErrorHandler {
}

private async logToCrashlytics(error: Error) {
const crashlyticsService = this.injector.get(CrashlyticsService);
if (crashlyticsService) {
const stacktrace = await getStacktraceFromError(error);
crashlyticsService.recordException({
message: error.message,
stacktrace,
});
}
const stacktrace = await getStacktraceFromError(error);
return this.crashlyticsService.recordException({
message: error.message,
stacktrace,
});
}
}
Loading

0 comments on commit b52f5dd

Please sign in to comment.