diff --git a/packages/cli/src/lib/cmds/init.ts b/packages/cli/src/lib/cmds/init.ts index ba1f329c..df8b41e1 100644 --- a/packages/cli/src/lib/cmds/init.ts +++ b/packages/cli/src/lib/cmds/init.ts @@ -17,7 +17,8 @@ import * as fs from 'fs'; import Color = require('color'); import * as inquirer from 'inquirer'; -import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, util} from '@bubblewrap/core'; +import {Config, DisplayModes, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, + util} from '@bubblewrap/core'; import {validateColor, validateKeyPassword, validateUrl, notEmpty} from '../inputHelpers'; import {ParsedArgs} from 'minimist'; import {APP_NAME} from '../constants'; @@ -49,6 +50,12 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise message: 'Name to be shown on the Android Launcher:', default: twaManifest.launcherName, validate: async (input): Promise => notEmpty(input, 'Launcher name'), + }, { + name: 'display', + type: 'list', + message: 'Display mode to be used:', + default: twaManifest.display, + choices: DisplayModes, }, { name: 'themeColor', type: 'input', @@ -131,6 +138,7 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise twaManifest.host = result.host; twaManifest.name = result.name; twaManifest.launcherName = result.launcherName; + twaManifest.display = result.display; twaManifest.themeColor = new Color(result.themeColor); twaManifest.backgroundColor = new Color(result.backgroundColor); twaManifest.startUrl = result.startUrl; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0fe2e10a..3042e66b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,7 +20,7 @@ import {GradleWrapper} from './lib/GradleWrapper'; import Log from './lib/Log'; import {JdkHelper} from './lib/jdk/JdkHelper'; import {KeyTool} from './lib/jdk/KeyTool'; -import {TwaManifest} from './lib/TwaManifest'; +import {TwaManifest, DisplayModes} from './lib/TwaManifest'; import {TwaGenerator} from './lib/TwaGenerator'; import {DigitalAssetLinks} from './lib/DigitalAssetLinks'; import * as util from './lib/util'; @@ -34,5 +34,6 @@ export {AndroidSdkTools, Log, TwaGenerator, TwaManifest, + DisplayModes, util, }; diff --git a/packages/core/src/lib/TwaManifest.ts b/packages/core/src/lib/TwaManifest.ts index e0764f9f..d4c60310 100644 --- a/packages/core/src/lib/TwaManifest.ts +++ b/packages/core/src/lib/TwaManifest.ts @@ -35,9 +35,19 @@ const MIN_SHORTCUT_ICON_SIZE = 96; // The minimum size needed for the notification icon const MIN_NOTIFICATION_ICON_SIZE = 48; +// Supported display modes for TWA +const DISPLAY_MODE_VALUES = ['standalone', 'fullscreen']; +type DisplayMode = typeof DISPLAY_MODE_VALUES[number]; +export const DisplayModes: DisplayMode[] = [...DISPLAY_MODE_VALUES]; + +export function asDisplayMode(input: string): DisplayMode | null { + return DISPLAY_MODE_VALUES.includes(input) ? input as DisplayMode : null; +} + // Default values used on the Twa Manifest const DEFAULT_SPLASHSCREEN_FADEOUT_DURATION = 300; const DEFAULT_APP_NAME = 'My TWA'; +const DEFAULT_DISPLAY_MODE = 'standalone'; const DEFAULT_THEME_COLOR = '#FFFFFF'; const DEFAULT_NAVIGATION_COLOR = '#000000'; const DEFAULT_BACKGROUND_COLOR = '#FFFFFF'; @@ -72,6 +82,7 @@ export class ShortcutInfo { * hostName: '<%= host %>', // The domain being opened in the TWA. * launchUrl: '<%= startUrl %>', // The start path for the TWA. Must be relative to the domain. * name: '<%= name %>', // The name shown on the Android Launcher. + * display: '<%= display %>', // The display mode for the TWA. * themeColor: '<%= themeColor %>', // The color used for the status bar. * navigationColor: '<%= themeColor %>', // The color used for the navigation bar. * backgroundColor: '<%= backgroundColor %>', // The color used for the splash screen background. @@ -94,6 +105,7 @@ export class TwaManifest { host: string; name: string; launcherName: string; + display: DisplayMode; themeColor: Color; navigationColor: Color; backgroundColor: Color; @@ -117,7 +129,10 @@ export class TwaManifest { this.packageId = data.packageId; this.host = data.host; this.name = data.name; - this.launcherName = data.launcherName || data.name; // Older Manifests may not have this field. + // Older manifests may not have this field: + this.launcherName = data.launcherName || data.name; + // Older manifests may not have this field: + this.display = asDisplayMode(data.display!) || DEFAULT_DISPLAY_MODE; this.themeColor = new Color(data.themeColor); this.navigationColor = new Color(data.navigationColor); this.backgroundColor = new Color(data.backgroundColor); @@ -254,6 +269,7 @@ export class TwaManifest { name: webManifest['name'] || webManifest['short_name'] || DEFAULT_APP_NAME, launcherName: webManifest['short_name'] || webManifest['name']?.substring(0, SHORT_NAME_MAX_SIZE) || DEFAULT_APP_NAME, + display: asDisplayMode(webManifest['display']!) || DEFAULT_DISPLAY_MODE, themeColor: webManifest['theme_color'] || DEFAULT_THEME_COLOR, navigationColor: DEFAULT_NAVIGATION_COLOR, backgroundColor: webManifest['background_color'] || DEFAULT_BACKGROUND_COLOR, @@ -306,6 +322,7 @@ export interface TwaManifestJson { host: string; name: string; launcherName?: string; // Older Manifests may not have this field. + display?: string; // Older Manifests may not have this field. themeColor: string; navigationColor: string; backgroundColor: string; diff --git a/packages/core/src/lib/types/WebManifest.ts b/packages/core/src/lib/types/WebManifest.ts index d28c136a..92250230 100644 --- a/packages/core/src/lib/types/WebManifest.ts +++ b/packages/core/src/lib/types/WebManifest.ts @@ -30,10 +30,13 @@ export interface WebManifestShortcutJson { icons?: Array; } +type WebManifestDisplayMode = 'browser' | 'minimal-ui' | 'standalone' | 'fullscreen'; + export interface WebManifestJson { name?: string; short_name?: string; start_url?: string; + display?: WebManifestDisplayMode; theme_color?: string; background_color?: string; icons?: Array; diff --git a/packages/core/src/spec/lib/TwaManifestSpec.ts b/packages/core/src/spec/lib/TwaManifestSpec.ts index 95272b9c..c27ea1ff 100644 --- a/packages/core/src/spec/lib/TwaManifestSpec.ts +++ b/packages/core/src/spec/lib/TwaManifestSpec.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import {TwaManifest, TwaManifestJson} from '../../lib/TwaManifest'; +import {TwaManifest, TwaManifestJson, asDisplayMode} from '../../lib/TwaManifest'; +import {WebManifestJson} from '../../lib/types/WebManifest'; import Color = require('color'); describe('TwaManifest', () => { @@ -24,6 +25,7 @@ describe('TwaManifest', () => { 'name': 'PWA Directory', 'short_name': 'PwaDirectory', 'start_url': '/?utm_source=homescreen', + 'display': 'fullscreen', 'icons': [{ 'src': '/favicons/android-chrome-192x192.png', 'sizes': '192x192', @@ -46,10 +48,11 @@ describe('TwaManifest', () => { }], }; const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); - const twaManifest = TwaManifest.fromWebManifestJson(manifestUrl, manifest); + const twaManifest = TwaManifest.fromWebManifestJson(manifestUrl, manifest as WebManifestJson); expect(twaManifest.packageId).toBe('com.pwa_directory.twa'); expect(twaManifest.name).toBe('PWA Directory'); expect(twaManifest.launcherName).toBe('PwaDirectory'); + expect(twaManifest.display).toBe('fullscreen'); expect(twaManifest.startUrl).toBe('/?utm_source=homescreen'); expect(twaManifest.iconUrl) .toBe('https://pwa-directory.com/favicons/android-chrome-512x512.png'); @@ -89,6 +92,7 @@ describe('TwaManifest', () => { expect(twaManifest.iconUrl).toBeUndefined(); expect(twaManifest.maskableIconUrl).toBeUndefined(); expect(twaManifest.monochromeIconUrl).toBeUndefined(); + expect(twaManifest.display).toBe('standalone'); expect(twaManifest.themeColor.hex()).toBe('#FFFFFF'); expect(twaManifest.navigationColor.hex()).toBe('#000000'); expect(twaManifest.backgroundColor.hex()).toBe('#FFFFFF'); @@ -165,6 +169,14 @@ describe('TwaManifest', () => { expect(twaManifest.maskableIconUrl).toBe('https://pwa-directory.com/favicons/maskable.png'); expect(twaManifest.monochromeIconUrl).toBe('https://pwa-directory.com/favicons/monochrome.png'); }); + + it('Replaces unsupported display modes with `standalone`', () => { + const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); + expect(TwaManifest.fromWebManifestJson(manifestUrl, {display: 'minimal-ui'}).display) + .toBe('standalone'); + expect(TwaManifest.fromWebManifestJson(manifestUrl, {display: 'browser'}).display) + .toBe('standalone'); + }); }); describe('#constructor', () => { @@ -176,6 +188,7 @@ describe('TwaManifest', () => { launcherName: 'PwaDirectory', startUrl: '/', iconUrl: 'https://pwa-directory.com/favicons/android-chrome-512x512.png', + display: 'fullscreen', themeColor: '#00ff00', navigationColor: '#000000', backgroundColor: '#0000ff', @@ -199,6 +212,7 @@ describe('TwaManifest', () => { expect(twaManifest.launcherName).toEqual(twaManifest.launcherName); expect(twaManifest.startUrl).toEqual(twaManifest.startUrl); expect(twaManifest.iconUrl).toEqual(twaManifest.iconUrl); + expect(twaManifest.display).toEqual('fullscreen'); expect(twaManifest.themeColor).toEqual(new Color('#00ff00')); expect(twaManifest.navigationColor).toEqual(new Color('#000000')); expect(twaManifest.backgroundColor).toEqual(new Color('#0000ff')); @@ -240,6 +254,7 @@ describe('TwaManifest', () => { const twaManifest = new TwaManifest(twaManifestJson); expect(twaManifest.webManifestUrl).toBeUndefined(); expect(twaManifest.fallbackType).toBe('customtabs'); + expect(twaManifest.display).toBe('standalone'); }); }); @@ -272,4 +287,18 @@ describe('TwaManifest', () => { expect(twaManifest.validate()).toBeNull(); }); }); + + describe('#asDisplayMode', () => { + it('Returns display mode if it is supported', () => { + expect(asDisplayMode('standalone')).toBe('standalone'); + expect(asDisplayMode('fullscreen')).toBe('fullscreen'); + }); + + it('Returns null for unsupported display modes', () => { + expect(asDisplayMode('browser')).toBeNull(); + expect(asDisplayMode('minimal-ui')).toBeNull(); + expect(asDisplayMode('bogus')).toBeNull(); + expect(asDisplayMode('')).toBeNull(); + }); + }); }); diff --git a/packages/core/template_project/app/src/main/AndroidManifest.xml b/packages/core/template_project/app/src/main/AndroidManifest.xml index 76a92a24..a04f2554 100644 --- a/packages/core/template_project/app/src/main/AndroidManifest.xml +++ b/packages/core/template_project/app/src/main/AndroidManifest.xml @@ -77,6 +77,11 @@ + <% if (display === 'fullscreen') { %> + + + <% }%>