mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Improvements
This commit is contained in:
1 parent
26ab6ace3b
commit
5f7dfe552f
8 files changed
+106
-17
No files matched your search
+4
-3
@@ -20,9 +20,10 @@ import { shouldOfferPwaInstall } from '@/utils/pwa.js';
|
||||
import { sendVisitBeacon } from '@/utils/pulse-beacon.js';
|
||||
import { useTheme } from '@/composables/use-theme.js';
|
||||
|
||||
// PWA install prompt — async and eligibility-gated: ineligible visits (first
|
||||
// visit, prompt cap reached, already installed) never load pwa-install or
|
||||
// trigger its manifest fetch; eligible ones load it at the prompt's 30s mark.
|
||||
// PWA install prompt — async and eligibility-gated: ineligible visits (too
|
||||
// few 12h-deduped uses, prompt cap reached, already installed) never load
|
||||
// pwa-install or trigger its manifest fetch; eligible ones load it at the
|
||||
// prompt's 30s mark.
|
||||
const PWA = defineAsyncComponent(() => import('@/components/widgets/PWA.vue'));
|
||||
const offerPwaInstall = ref(false);
|
||||
onMounted(() => {
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import '@khmyznikov/pwa-install';
|
||||
import { detectBrowser, detectOS } from '@/utils/system-detect.js';
|
||||
import { getPwaVisitCount } from '@/utils/pwa.js';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
// Define data
|
||||
const isDesktopChrome = ref(true);
|
||||
@@ -47,6 +51,8 @@ const showPWA = () => {
|
||||
pwaInstall.isAppleMobilePlatform = isIosSafari.value;
|
||||
pwaInstall.isAppleDesktopPlatform = isMacSafari.value;
|
||||
pwaInstall.externalPromptEvent = window.ipcheckInstallPromptEvent || pwaInstall.externalPromptEvent;
|
||||
pwaInstall.installDescription = t('pwa.installDescription', { count: getPwaVisitCount() });
|
||||
pwaInstall.description = t('pwa.appDescription');
|
||||
|
||||
if (!pwaInstall.isUnderStandaloneMode && pwaInstall.isInstallAvailable) {
|
||||
pwaInstall.showDialog(true);
|
||||
|
||||
@@ -1186,5 +1186,9 @@
|
||||
"add": "Added",
|
||||
"improve": "Improved",
|
||||
"fix": "Fixed"
|
||||
},
|
||||
"pwa": {
|
||||
"appDescription": "All in one IP Toolbox",
|
||||
"installDescription": "You've already used IPCheck.ing {count} times — install it as an app for a smoother experience and quicker access."
|
||||
}
|
||||
}
|
||||
@@ -1186,5 +1186,9 @@
|
||||
"add": "Ajouté",
|
||||
"improve": "Amélioré",
|
||||
"fix": "Corrigé"
|
||||
},
|
||||
"pwa": {
|
||||
"appDescription": "La boîte à outils IP tout-en-un",
|
||||
"installDescription": "Vous avez déjà utilisé IPCheck.ing {count} fois — installez-le comme application pour une expérience plus fluide et un accès plus rapide."
|
||||
}
|
||||
}
|
||||
@@ -1186,5 +1186,9 @@
|
||||
"add": "Добавлено",
|
||||
"improve": "Улучшено",
|
||||
"fix": "Исправлено"
|
||||
},
|
||||
"pwa": {
|
||||
"appDescription": "Универсальный набор IP-инструментов",
|
||||
"installDescription": "Вы открываете IPCheck.ing уже в {count}-й раз — установите его как приложение, чтобы пользоваться им быстрее и удобнее."
|
||||
}
|
||||
}
|
||||
@@ -1186,5 +1186,9 @@
|
||||
"add": "新增",
|
||||
"improve": "优化",
|
||||
"fix": "修复"
|
||||
},
|
||||
"pwa": {
|
||||
"appDescription": "一站式 IP 工具箱",
|
||||
"installDescription": "这已经是你第 {count} 次使用 IPCheck.ing 了,把它安装为应用,可以获得更好的使用体验,打开也更方便。"
|
||||
}
|
||||
}
|
||||
+26
-6
@@ -16,16 +16,36 @@ export function isRunningAsPwa() {
|
||||
|
||||
// Proactive install-prompt eligibility, checked by App.vue BEFORE mounting
|
||||
// the PWA widget — ineligible visits never load pwa-install or its manifest
|
||||
// fetch. Counts this visit as a side effect: the prompt skips the very first
|
||||
// visit and is capped at 2 shows ever (PWA.vue bumps pwaPopupCount when the
|
||||
// dialog actually opens).
|
||||
// fetch. Counts this visit as a side effect, deduped by a 12h window: opens
|
||||
// within 12h of the last counted one belong to the same "use", so the count
|
||||
// means returning sessions, not reloads. The prompt skips the first use and
|
||||
// may show on the 2nd and 3rd (`popups < visits - 1` blocks a same-window
|
||||
// re-prompt, which also spaces the two shows ≥12h apart); capped at 2 shows
|
||||
// ever (PWA.vue bumps pwaPopupCount when the dialog actually opens).
|
||||
const USE_WINDOW_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
export const shouldOfferPwaInstall = () => {
|
||||
try {
|
||||
const visits = parseInt(localStorage.getItem('pwaVisitCount') || '0', 10) + 1;
|
||||
localStorage.setItem('pwaVisitCount', visits);
|
||||
let visits = parseInt(localStorage.getItem('pwaVisitCount') || '0', 10);
|
||||
const lastVisitAt = parseInt(localStorage.getItem('pwaLastVisitAt') || '0', 10);
|
||||
const now = Date.now();
|
||||
if (visits === 0 || now - lastVisitAt >= USE_WINDOW_MS) {
|
||||
visits += 1;
|
||||
localStorage.setItem('pwaVisitCount', visits);
|
||||
localStorage.setItem('pwaLastVisitAt', now);
|
||||
}
|
||||
const popups = parseInt(localStorage.getItem('pwaPopupCount') || '0', 10);
|
||||
return !isRunningAsPwa() && visits >= 2 && popups < 2;
|
||||
return !isRunningAsPwa() && popups < 2 && visits >= popups + 2;
|
||||
} catch {
|
||||
return false; // storage disabled — never prompt
|
||||
}
|
||||
};
|
||||
|
||||
// Deduped use count for the install-prompt copy ("your {count}th visit").
|
||||
export const getPwaVisitCount = () => {
|
||||
try {
|
||||
return parseInt(localStorage.getItem('pwaVisitCount') || '0', 10);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
+54
-8
@@ -15,7 +15,15 @@ globalThis.__pwaDisplayMode = false;
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import { shouldOfferPwaInstall } from '../frontend/utils/pwa.js';
|
||||
import { shouldOfferPwaInstall, getPwaVisitCount } from '../frontend/utils/pwa.js';
|
||||
|
||||
const HOURS = 60 * 60 * 1000;
|
||||
// Seed storage as if the previous counted use happened `agoMs` ago.
|
||||
const seed = (visits, agoMs, popups = 0) => {
|
||||
globalThis.localStorage.setItem('pwaVisitCount', visits);
|
||||
globalThis.localStorage.setItem('pwaLastVisitAt', Date.now() - agoMs);
|
||||
globalThis.localStorage.setItem('pwaPopupCount', popups);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.localStorage.clear();
|
||||
@@ -23,33 +31,71 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('shouldOfferPwaInstall', () => {
|
||||
it('skips the very first visit but still counts it', () => {
|
||||
it('skips the very first use but still counts and stamps it', () => {
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '1');
|
||||
assert.ok(parseInt(globalThis.localStorage.getItem('pwaLastVisitAt'), 10) > 0);
|
||||
});
|
||||
|
||||
it('does not count reloads inside the 12h window', () => {
|
||||
shouldOfferPwaInstall();
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '1');
|
||||
});
|
||||
|
||||
it('offers from the second visit onward', () => {
|
||||
shouldOfferPwaInstall();
|
||||
it('offers on the second use, 12h or more after the first', () => {
|
||||
seed(1, 13 * HOURS);
|
||||
assert.equal(shouldOfferPwaInstall(), true);
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '2');
|
||||
});
|
||||
|
||||
it('does not re-prompt on a same-window reload after a show', () => {
|
||||
// The visit-2 prompt just fired: PWA.vue bumped pwaPopupCount to 1.
|
||||
seed(2, 1 * HOURS, 1);
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
});
|
||||
|
||||
it('offers a second time on the third use', () => {
|
||||
seed(2, 13 * HOURS, 1);
|
||||
assert.equal(shouldOfferPwaInstall(), true);
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '3');
|
||||
});
|
||||
|
||||
it('stops offering once the popup cap (2) is reached', () => {
|
||||
globalThis.localStorage.setItem('pwaVisitCount', '5');
|
||||
globalThis.localStorage.setItem('pwaPopupCount', '2');
|
||||
seed(5, 13 * HOURS, 2);
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
});
|
||||
|
||||
it('never offers inside an installed PWA window', () => {
|
||||
globalThis.localStorage.setItem('pwaVisitCount', '5');
|
||||
seed(5, 13 * HOURS);
|
||||
globalThis.__pwaDisplayMode = true;
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
// The visit still counts even when running as a PWA.
|
||||
// The use still counts even when running as a PWA.
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '6');
|
||||
});
|
||||
|
||||
it('treats a pre-timestamp legacy count as an expired window', () => {
|
||||
// Upgraded visitor: pwaVisitCount exists, pwaLastVisitAt does not.
|
||||
globalThis.localStorage.setItem('pwaVisitCount', '4');
|
||||
assert.equal(shouldOfferPwaInstall(), true);
|
||||
assert.equal(globalThis.localStorage.getItem('pwaVisitCount'), '5');
|
||||
});
|
||||
|
||||
it('returns false when storage is unavailable', () => {
|
||||
globalThis.localStorage._throw = true;
|
||||
assert.equal(shouldOfferPwaInstall(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPwaVisitCount', () => {
|
||||
it('reads the stored count and defaults to 0', () => {
|
||||
assert.equal(getPwaVisitCount(), 0);
|
||||
globalThis.localStorage.setItem('pwaVisitCount', '3');
|
||||
assert.equal(getPwaVisitCount(), 3);
|
||||
});
|
||||
|
||||
it('returns 0 when storage is unavailable', () => {
|
||||
globalThis.localStorage._throw = true;
|
||||
assert.equal(getPwaVisitCount(), 0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user