@dotrino/install 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.d.ts +31 -3
- package/src/index.js +219 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/install",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Botón de \"Instalar app\" (PWA) unificado del ecosistema Dotrino: Web Component <dotrino-install> que captura beforeinstallprompt temprano, maneja iOS con un modal de instrucciones (sin alert) y se oculta si ya está instalada. Vue o vanilla. Sin JS de terceros ni cookies, bilingüe es/en.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
export type InstallOutcome = 'accepted' | 'dismissed' | 'instructions' | 'installed'
|
|
1
|
+
export type InstallOutcome = 'accepted' | 'dismissed' | 'instructions' | 'installed' | 'relaunch'
|
|
2
|
+
export type InstallContext = 'installed' | 'native' | 'ios' | 'relaunch' | 'none'
|
|
2
3
|
|
|
3
4
|
/** ¿La app ya corre como instalada (display-mode standalone) o se instaló ya? */
|
|
4
5
|
export function isAppInstalled(): boolean
|
|
5
6
|
|
|
7
|
+
/** Atajo legible de `isAppInstalled()`. */
|
|
8
|
+
export function isStandalone(): boolean
|
|
9
|
+
|
|
6
10
|
/** Detección de iOS/iPadOS (incluye el iPad que se reporta como Mac). */
|
|
7
11
|
export function isIOS(): boolean
|
|
8
12
|
|
|
13
|
+
/** Detección de Android. */
|
|
14
|
+
export function isAndroid(): boolean
|
|
15
|
+
|
|
16
|
+
/** Nombre del parámetro marcador ("pwa-install"). */
|
|
17
|
+
export const INSTALL_PARAM: string
|
|
18
|
+
|
|
19
|
+
/** ¿La URL trae el marcador `?pwa-install=1` (tras relanzar en Chrome)? */
|
|
20
|
+
export function hasInstallFlag(param?: string): boolean
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `intent://` que reabre la app en Chrome (no el Custom Tab/webview embebido)
|
|
24
|
+
* con `?pwa-install=1`, con fallback a https. Solo útil en Android. null si falla.
|
|
25
|
+
*/
|
|
26
|
+
export function chromeInstallUrl(param?: string): string | null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Estado de cara a la UI: 'installed' | 'native' (prompt nativo) | 'ios'
|
|
30
|
+
* (instrucciones Safari) | 'relaunch' (Android embebido → abrir en Chrome) | 'none'.
|
|
31
|
+
*/
|
|
32
|
+
export function installContext(): InstallContext
|
|
33
|
+
|
|
9
34
|
/** ¿Tiene sentido ofrecer instalar? (prompt nativo disponible, o iOS, y no instalada). */
|
|
10
35
|
export function canInstall(): boolean
|
|
11
36
|
|
|
@@ -26,8 +51,11 @@ export const HOME_DEFAULT: string
|
|
|
26
51
|
|
|
27
52
|
/**
|
|
28
53
|
* Custom element del botón de instalar (`<dotrino-install>`).
|
|
29
|
-
* Atributos: `lang` ("es"|"en"), `label` (texto), `icon` ("false" oculta el icono)
|
|
30
|
-
*
|
|
54
|
+
* Atributos: `lang` ("es"|"en"), `label` (texto), `icon` ("false" oculta el icono),
|
|
55
|
+
* `app-name` y `app-icon` (título e icono del overlay grande de instalación).
|
|
56
|
+
* En Android dentro de un Custom Tab (sin prompt nativo) el botón relanza la app
|
|
57
|
+
* en Chrome; al llegar con `?pwa-install=1` muestra un overlay grande centrado.
|
|
58
|
+
* Eventos: `cc-install` (cancelable), `cc-install-result` (detail.outcome, incluye 'relaunch').
|
|
31
59
|
*/
|
|
32
60
|
export class DotrinoInstall extends HTMLElement {
|
|
33
61
|
/** Dispara la instalación desde JS (igual que el click del usuario). */
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* recurrentes que aquí se arreglan de una vez:
|
|
8
8
|
*
|
|
9
9
|
* 1. El evento `beforeinstallprompt` se dispara MUY pronto (a veces antes de
|
|
10
|
-
* montar el componente). Si lo
|
|
10
|
+
* montar el componente). Si lo escuchas dentro de onMounted lo pierdes y el
|
|
11
11
|
* botón nunca aparece. Aquí lo capturamos a nivel de módulo, en import.
|
|
12
12
|
* 2. iOS/Safari NO dispara `beforeinstallprompt` y no hay API de instalación:
|
|
13
13
|
* la única vía es "Compartir → Añadir a pantalla de inicio". Mostramos esas
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* <dotrino-install></dotrino-install>
|
|
25
25
|
* <dotrino-install lang="en" label="Install"></dotrino-install>
|
|
26
26
|
*
|
|
27
|
-
* Uso programático (si
|
|
27
|
+
* Uso programático (si quieres tu propio botón) — ver también ./vue:
|
|
28
28
|
* import { canInstall, promptInstall, onInstallStateChange } from '@dotrino/install'
|
|
29
29
|
* if (canInstall()) showMyButton()
|
|
30
30
|
* await promptInstall() // dispara el prompt nativo o el modal iOS/fallback
|
|
@@ -41,6 +41,7 @@ const HOME_DEFAULT = 'https://dotrino.com'
|
|
|
41
41
|
|
|
42
42
|
let _deferred = null // el BeforeInstallPromptEvent diferido (o null)
|
|
43
43
|
let _installed = false // se marcó appinstalled en esta sesión
|
|
44
|
+
let _settled = false // pasó el margen de espera del prompt nativo
|
|
44
45
|
const _subs = new Set() // suscriptores a cambios de estado (re-render)
|
|
45
46
|
|
|
46
47
|
function _emit () {
|
|
@@ -49,6 +50,13 @@ function _emit () {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// Tras un margen sin prompt nativo (Android), asumimos contexto embebido (Custom
|
|
54
|
+
// Tab abierto desde otra PWA): ahí Chrome NO dispara `beforeinstallprompt`, así
|
|
55
|
+
// que en vez de ocultar el botón ofreceremos relanzar en Chrome (ver más abajo).
|
|
56
|
+
if (typeof window !== 'undefined') {
|
|
57
|
+
try { setTimeout(() => { _settled = true; _emit() }, 1400) } catch (_) {}
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
function _onBIP (e) {
|
|
53
61
|
// Evita que el navegador muestre su mini-infobar; nosotros decidimos cuándo.
|
|
54
62
|
try { e.preventDefault() } catch (_) {}
|
|
@@ -91,6 +99,55 @@ export function isIOS () {
|
|
|
91
99
|
return false
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
/** ¿Corre como app instalada (standalone)? Atajo legible. */
|
|
103
|
+
export function isStandalone () {
|
|
104
|
+
return isAppInstalled()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Detección de Android. */
|
|
108
|
+
export function isAndroid () {
|
|
109
|
+
try { return /Android/i.test(navigator.userAgent || '') } catch (_) { return false }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const INSTALL_PARAM = 'pwa-install'
|
|
113
|
+
|
|
114
|
+
/** ¿La URL trae el marcador de "abrir para instalar" (tras relanzar en Chrome)? */
|
|
115
|
+
export function hasInstallFlag (param = INSTALL_PARAM) {
|
|
116
|
+
try { return new URLSearchParams(location.search).has(param) } catch (_) { return false }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Construye un `intent://` que reabre ESTA misma app en **Chrome** (no en el
|
|
121
|
+
* Custom Tab/webview embebido) con el marcador `?pwa-install=1`, con fallback a
|
|
122
|
+
* la URL https normal si Chrome no está. Solo tiene sentido en Android.
|
|
123
|
+
*/
|
|
124
|
+
export function chromeInstallUrl (param = INSTALL_PARAM) {
|
|
125
|
+
try {
|
|
126
|
+
const u = new URL(location.href)
|
|
127
|
+
u.searchParams.set(param, '1')
|
|
128
|
+
const target = `${u.host}${u.pathname}${u.search}`
|
|
129
|
+
const fallback = encodeURIComponent(u.toString())
|
|
130
|
+
return `intent://${target}#Intent;scheme=https;package=com.android.chrome;S.browser_fallback_url=${fallback};end`
|
|
131
|
+
} catch (_) { return null }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Estado de instalación de cara a la UI:
|
|
136
|
+
* - 'installed' ya instalada / standalone → no ofrecer.
|
|
137
|
+
* - 'native' hay prompt nativo (Chrome instalable) → instalar de un toque.
|
|
138
|
+
* - 'ios' iOS/Safari → instrucciones "Añadir a pantalla de inicio".
|
|
139
|
+
* - 'relaunch' Android sin prompt nativo (probable Custom Tab embebido) →
|
|
140
|
+
* relanzar en Chrome con `chromeInstallUrl()`.
|
|
141
|
+
* - 'none' nada que ofrecer (todavía esperando, o navegador sin soporte).
|
|
142
|
+
*/
|
|
143
|
+
export function installContext () {
|
|
144
|
+
if (isAppInstalled()) return 'installed'
|
|
145
|
+
if (_deferred) return 'native'
|
|
146
|
+
if (isIOS()) return 'ios'
|
|
147
|
+
if (_settled && isAndroid()) return 'relaunch'
|
|
148
|
+
return 'none'
|
|
149
|
+
}
|
|
150
|
+
|
|
94
151
|
/**
|
|
95
152
|
* ¿Tiene sentido ofrecer instalar? true si hay prompt nativo disponible, o si es
|
|
96
153
|
* iOS (instalable a mano vía Compartir), siempre que NO esté ya instalada.
|
|
@@ -153,7 +210,12 @@ const I18N = {
|
|
|
153
210
|
iosStep2: 'Elige «Añadir a pantalla de inicio»',
|
|
154
211
|
otherIntro: 'Tu navegador no permite la instalación con un toque. Para instalarla:',
|
|
155
212
|
otherStep: 'Abre el menú del navegador y elige «Instalar app» (o «Añadir a pantalla de inicio»).',
|
|
156
|
-
close: 'Cerrar'
|
|
213
|
+
close: 'Cerrar',
|
|
214
|
+
bigTitle: 'Instala la app',
|
|
215
|
+
bigTitleNamed: (n) => `Instala ${n}`,
|
|
216
|
+
bigSub: 'Acceso directo en tu pantalla de inicio, sin tienda de apps.',
|
|
217
|
+
preparing: 'Preparando…',
|
|
218
|
+
notNow: 'Ahora no'
|
|
157
219
|
},
|
|
158
220
|
en: {
|
|
159
221
|
install: 'Install App',
|
|
@@ -163,7 +225,12 @@ const I18N = {
|
|
|
163
225
|
iosStep2: 'Choose “Add to Home Screen”',
|
|
164
226
|
otherIntro: 'Your browser can’t install with one tap. To install it:',
|
|
165
227
|
otherStep: 'Open the browser menu and choose “Install app” (or “Add to Home Screen”).',
|
|
166
|
-
close: 'Close'
|
|
228
|
+
close: 'Close',
|
|
229
|
+
bigTitle: 'Install the app',
|
|
230
|
+
bigTitleNamed: (n) => `Install ${n}`,
|
|
231
|
+
bigSub: 'A shortcut on your home screen, no app store.',
|
|
232
|
+
preparing: 'Preparing…',
|
|
233
|
+
notNow: 'Not now'
|
|
167
234
|
}
|
|
168
235
|
}
|
|
169
236
|
|
|
@@ -263,25 +330,47 @@ const STYLE = `
|
|
|
263
330
|
flex: none; width: 24px; height: 24px; border-radius: 50%;
|
|
264
331
|
display: inline-flex; align-items: center; justify-content: center;
|
|
265
332
|
font-size: 13px; font-weight: 700;
|
|
266
|
-
background: var(--cc-install-accent, #84cc16); color: #14110f;
|
|
333
|
+
background: var(--cc-install-accent, #84cc16); color: var(--cc-install-accent-color, #14110f);
|
|
267
334
|
}
|
|
268
335
|
.steps svg { width: 20px; height: 20px; flex: none; }
|
|
269
336
|
.card .ok {
|
|
270
337
|
all: unset; box-sizing: border-box; cursor: pointer;
|
|
271
338
|
display: block; width: 100%; text-align: center;
|
|
272
339
|
padding: 11px; border-radius: 12px; font-weight: 700;
|
|
273
|
-
background: var(--cc-install-accent, #84cc16); color: #14110f;
|
|
340
|
+
background: var(--cc-install-accent, #84cc16); color: var(--cc-install-accent-color, #14110f);
|
|
274
341
|
}
|
|
275
342
|
.card .ok:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
343
|
+
|
|
344
|
+
/* Overlay grande y centrado: aparece al llegar con ?pwa-install=1 (relanzado en Chrome). */
|
|
345
|
+
.big { text-align: center; }
|
|
346
|
+
.big .app-icon { width: 72px; height: 72px; border-radius: 18px; margin: 2px auto 14px; display: block; }
|
|
347
|
+
.big h2 { font-size: 20px; }
|
|
348
|
+
.big .sub { margin: 0 0 18px; opacity: .8; }
|
|
349
|
+
.big .cta {
|
|
350
|
+
all: unset; box-sizing: border-box; cursor: pointer;
|
|
351
|
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
352
|
+
width: 100%; padding: 14px; border-radius: 14px; font-weight: 800; font-size: 16px;
|
|
353
|
+
background: var(--cc-install-accent, #84cc16); color: var(--cc-install-accent-color, #14110f);
|
|
354
|
+
}
|
|
355
|
+
.big .cta[disabled] { opacity: .6; cursor: default; }
|
|
356
|
+
.big .cta svg { width: 20px; height: 20px; }
|
|
357
|
+
.big .manual { margin: 14px 2px 0; font-size: 14px; opacity: .85; }
|
|
358
|
+
.big .dismiss {
|
|
359
|
+
all: unset; box-sizing: border-box; cursor: pointer;
|
|
360
|
+
display: block; width: 100%; text-align: center;
|
|
361
|
+
margin-top: 10px; padding: 8px; font-weight: 600; opacity: .7;
|
|
362
|
+
}
|
|
276
363
|
`
|
|
277
364
|
|
|
278
365
|
class DotrinoInstall extends HTMLElement {
|
|
279
|
-
static get observedAttributes () { return ['lang', 'label', 'icon'] }
|
|
366
|
+
static get observedAttributes () { return ['lang', 'label', 'icon', 'app-name', 'app-icon'] }
|
|
280
367
|
|
|
281
368
|
constructor () {
|
|
282
369
|
super()
|
|
283
370
|
this.attachShadow({ mode: 'open' })
|
|
284
|
-
this._modalOpen = false
|
|
371
|
+
this._modalOpen = false // modal de instrucciones (iOS/fallback)
|
|
372
|
+
this._bigOpen = false // overlay grande de instalación (?pwa-install=1)
|
|
373
|
+
this._bigManual = false // el overlay grande pasó a instrucciones manuales
|
|
285
374
|
this._unsub = null
|
|
286
375
|
this._onState = this._render.bind(this)
|
|
287
376
|
this._onKey = this._onKey.bind(this)
|
|
@@ -289,12 +378,52 @@ class DotrinoInstall extends HTMLElement {
|
|
|
289
378
|
|
|
290
379
|
connectedCallback () {
|
|
291
380
|
this._unsub = onInstallStateChange(this._onState)
|
|
381
|
+
// Si llegamos con el marcador (relanzados en Chrome para instalar), abrimos
|
|
382
|
+
// el overlay grande centrado. Solo el primero monta el overlay (singleton).
|
|
383
|
+
if (hasInstallFlag() && !isAppInstalled() && !DotrinoInstall._bigClaimed) {
|
|
384
|
+
DotrinoInstall._bigClaimed = true
|
|
385
|
+
this._bigOpen = true
|
|
386
|
+
try { document.addEventListener('keydown', this._onKey) } catch (_) {}
|
|
387
|
+
// Si en unos segundos no hay prompt nativo, ofrecemos la vía manual.
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
if (this._bigOpen && !hasNativePrompt() && !isAppInstalled()) { this._bigManual = true; this._render() }
|
|
390
|
+
}, 4000)
|
|
391
|
+
}
|
|
292
392
|
this._render()
|
|
293
393
|
}
|
|
294
394
|
|
|
295
395
|
disconnectedCallback () {
|
|
296
396
|
if (this._unsub) { this._unsub(); this._unsub = null }
|
|
297
397
|
try { document.removeEventListener('keydown', this._onKey) } catch (_) {}
|
|
398
|
+
if (this._portal) { try { this._portal.remove() } catch (_) {} this._portal = null; this._portalShadow = null }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Los modales (instrucciones / overlay grande) usan position:fixed para
|
|
402
|
+
centrarse en la ventana. Si el <dotrino-install> vive dentro de un ancestro
|
|
403
|
+
con `backdrop-filter`/`transform` (topbars), ese ancestro se vuelve el bloque
|
|
404
|
+
contenedor del fixed y el modal se descoloca. Por eso los renderizamos en un
|
|
405
|
+
PORTAL colgado de <body>, con su propio shadow root y el tema copiado. */
|
|
406
|
+
_portalRoot () {
|
|
407
|
+
if (!this._portal) {
|
|
408
|
+
this._portal = document.createElement('div')
|
|
409
|
+
this._portal.setAttribute('data-dotrino-install-portal', '')
|
|
410
|
+
this._portalShadow = this._portal.attachShadow({ mode: 'open' })
|
|
411
|
+
try {
|
|
412
|
+
const cs = getComputedStyle(this)
|
|
413
|
+
for (const v of ['--cc-install-accent', '--cc-install-modal-bg', '--cc-install-modal-color']) {
|
|
414
|
+
const val = cs.getPropertyValue(v)
|
|
415
|
+
if (val && val.trim()) this._portal.style.setProperty(v, val.trim())
|
|
416
|
+
}
|
|
417
|
+
} catch (_) {}
|
|
418
|
+
document.body.appendChild(this._portal)
|
|
419
|
+
}
|
|
420
|
+
return this._portalShadow
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
_renderPortal (innerHTML) {
|
|
424
|
+
const root = this._portalRoot()
|
|
425
|
+
root.innerHTML = innerHTML ? `<style>${STYLE}</style>${innerHTML}` : ''
|
|
426
|
+
return root
|
|
298
427
|
}
|
|
299
428
|
|
|
300
429
|
attributeChangedCallback () {
|
|
@@ -302,39 +431,68 @@ class DotrinoInstall extends HTMLElement {
|
|
|
302
431
|
}
|
|
303
432
|
|
|
304
433
|
_render () {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
}
|
|
434
|
+
const ctx = installContext() // installed|native|ios|relaunch|none
|
|
435
|
+
const showBtn = ctx === 'native' || ctx === 'ios' || ctx === 'relaunch'
|
|
436
|
+
// Oculta el host por completo cuando no hay botón ni modal abierto.
|
|
437
|
+
this.hidden = !showBtn && !this._modalOpen && !this._bigOpen
|
|
438
|
+
if (this.hidden) { this.shadowRoot.innerHTML = ''; return }
|
|
312
439
|
|
|
313
440
|
const lang = resolveLang(this.getAttribute('lang'))
|
|
314
441
|
const t = I18N[lang]
|
|
315
442
|
const label = this.getAttribute('label') != null ? this.getAttribute('label') : t.install
|
|
316
443
|
const showIcon = (this.getAttribute('icon') || '').toLowerCase() !== 'false'
|
|
317
444
|
|
|
445
|
+
// Botón pequeño en el shadow propio (inline en la topbar).
|
|
318
446
|
let html = `<style>${STYLE}</style>`
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
447
|
+
if (showBtn) {
|
|
448
|
+
html += `<button class="trigger" type="button" part="button" aria-label="${label || t.install}">`
|
|
449
|
+
if (showIcon) html += `<span class="ico" part="icon">${ICON_DOWNLOAD}</span>`
|
|
450
|
+
html += `<span class="lbl" part="label">${label}</span></button>`
|
|
451
|
+
}
|
|
325
452
|
this.shadowRoot.innerHTML = html
|
|
326
453
|
const btn = this.shadowRoot.querySelector('button.trigger')
|
|
327
|
-
if (btn) btn.addEventListener('click', () => this._activate())
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
if (backdrop) backdrop.addEventListener('click', (e) => { if (e.target === backdrop)
|
|
334
|
-
|
|
454
|
+
if (btn) btn.addEventListener('click', () => this._activate(ctx))
|
|
455
|
+
|
|
456
|
+
// Modales en el PORTAL (body), para que el fixed se centre en la ventana.
|
|
457
|
+
if (this._bigOpen) {
|
|
458
|
+
const root = this._renderPortal(this._bigHTML(lang))
|
|
459
|
+
const backdrop = root.querySelector('.backdrop')
|
|
460
|
+
if (backdrop) backdrop.addEventListener('click', (e) => { if (e.target === backdrop) this._closeBig() })
|
|
461
|
+
root.querySelector('.dismiss')?.addEventListener('click', () => this._closeBig())
|
|
462
|
+
root.querySelector('.cta')?.addEventListener('click', () => this._bigInstall())
|
|
463
|
+
} else if (this._modalOpen) {
|
|
464
|
+
const root = this._renderPortal(this._modalHTML(lang))
|
|
465
|
+
const backdrop = root.querySelector('.backdrop')
|
|
466
|
+
const okBtn = root.querySelector('.ok')
|
|
467
|
+
if (backdrop) backdrop.addEventListener('click', (e) => { if (e.target === backdrop) this._closeModal() })
|
|
468
|
+
if (okBtn) okBtn.addEventListener('click', () => this._closeModal())
|
|
469
|
+
} else if (this._portalShadow) {
|
|
470
|
+
this._renderPortal('') // limpia el portal cuando no hay modal
|
|
335
471
|
}
|
|
336
472
|
}
|
|
337
473
|
|
|
474
|
+
/** Overlay grande y centrado de instalación (al llegar con ?pwa-install=1). */
|
|
475
|
+
_bigHTML (lang) {
|
|
476
|
+
const t = I18N[lang]
|
|
477
|
+
const name = this.getAttribute('app-name')
|
|
478
|
+
const icon = this.getAttribute('app-icon')
|
|
479
|
+
const title = name ? t.bigTitleNamed(name) : t.bigTitle
|
|
480
|
+
const ready = hasNativePrompt()
|
|
481
|
+
let body
|
|
482
|
+
if (this._bigManual) {
|
|
483
|
+
const step = isIOS() ? `${t.iosStep1} → ${t.iosStep2}` : t.otherStep
|
|
484
|
+
body = `<p class="manual">${step}</p>`
|
|
485
|
+
} else {
|
|
486
|
+
body = `<button class="cta" type="button" ${ready ? '' : 'disabled'}>${ICON_DOWNLOAD}` +
|
|
487
|
+
`<span>${ready ? t.install : t.preparing}</span></button>`
|
|
488
|
+
}
|
|
489
|
+
return `<div class="backdrop" part="modal" role="dialog" aria-modal="true" aria-label="${title}">` +
|
|
490
|
+
`<div class="card big" part="modal-card">` +
|
|
491
|
+
(icon ? `<img class="app-icon" src="${icon}" alt="" width="72" height="72" />` : '') +
|
|
492
|
+
`<h2>${title}</h2><p class="sub">${t.bigSub}</p>${body}` +
|
|
493
|
+
`<button class="dismiss" type="button">${t.notNow}</button></div></div>`
|
|
494
|
+
}
|
|
495
|
+
|
|
338
496
|
_modalHTML (lang) {
|
|
339
497
|
const t = I18N[lang]
|
|
340
498
|
let steps
|
|
@@ -350,10 +508,19 @@ class DotrinoInstall extends HTMLElement {
|
|
|
350
508
|
`<button class="ok" type="button">${t.close}</button></div></div>`
|
|
351
509
|
}
|
|
352
510
|
|
|
353
|
-
async _activate () {
|
|
511
|
+
async _activate (ctx) {
|
|
354
512
|
const ev = new CustomEvent('cc-install', { bubbles: true, composed: true, cancelable: true })
|
|
355
513
|
if (!this.dispatchEvent(ev)) return // la app canceló para hacer lo suyo
|
|
356
514
|
|
|
515
|
+
// Contexto embebido (Custom Tab desde otra PWA): no hay prompt nativo, así que
|
|
516
|
+
// relanzamos la app en Chrome, donde la instalación sí funciona.
|
|
517
|
+
if (ctx === 'relaunch') {
|
|
518
|
+
const url = chromeInstallUrl()
|
|
519
|
+
this.dispatchEvent(new CustomEvent('cc-install-result', { bubbles: true, composed: true, detail: { outcome: 'relaunch' } }))
|
|
520
|
+
if (url) { try { location.href = url } catch (_) {} }
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
357
524
|
const outcome = await promptInstall()
|
|
358
525
|
if (outcome === 'instructions') {
|
|
359
526
|
this._openModal()
|
|
@@ -364,6 +531,22 @@ class DotrinoInstall extends HTMLElement {
|
|
|
364
531
|
this._render()
|
|
365
532
|
}
|
|
366
533
|
|
|
534
|
+
/** Botón grande del overlay: dispara el prompt nativo; si no hay, instrucciones. */
|
|
535
|
+
async _bigInstall () {
|
|
536
|
+
if (!hasNativePrompt()) { this._bigManual = true; this._render(); return }
|
|
537
|
+
const outcome = await promptInstall()
|
|
538
|
+
this.dispatchEvent(new CustomEvent('cc-install-result', { bubbles: true, composed: true, detail: { outcome } }))
|
|
539
|
+
if (outcome === 'accepted' || outcome === 'installed') this._closeBig()
|
|
540
|
+
else if (outcome === 'instructions') { this._bigManual = true; this._render() }
|
|
541
|
+
else this._render()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
_closeBig () {
|
|
545
|
+
this._bigOpen = false
|
|
546
|
+
try { document.removeEventListener('keydown', this._onKey) } catch (_) {}
|
|
547
|
+
this._render()
|
|
548
|
+
}
|
|
549
|
+
|
|
367
550
|
_openModal () {
|
|
368
551
|
this._modalOpen = true
|
|
369
552
|
try { document.addEventListener('keydown', this._onKey) } catch (_) {}
|
|
@@ -377,7 +560,9 @@ class DotrinoInstall extends HTMLElement {
|
|
|
377
560
|
}
|
|
378
561
|
|
|
379
562
|
_onKey (e) {
|
|
380
|
-
if (e.key
|
|
563
|
+
if (e.key !== 'Escape') return
|
|
564
|
+
if (this._bigOpen) this._closeBig()
|
|
565
|
+
else this._closeModal()
|
|
381
566
|
}
|
|
382
567
|
|
|
383
568
|
/** Dispara la instalación desde JS de la app (igual que el click). */
|
|
@@ -388,4 +573,6 @@ if (typeof customElements !== 'undefined' && !customElements.get('dotrino-instal
|
|
|
388
573
|
customElements.define('dotrino-install', DotrinoInstall)
|
|
389
574
|
}
|
|
390
575
|
|
|
391
|
-
|
|
576
|
+
DotrinoInstall._bigClaimed = false
|
|
577
|
+
|
|
578
|
+
export { DotrinoInstall, HOME_DEFAULT, INSTALL_PARAM }
|