Improvements

This commit is contained in:
jason5ng32 committed 2026-08-16 15:29:00 +08:00
1 parent 26ab6ace3b
commit 5f7dfe552f
8 files changed
+106 -17

No files matched your search

+4 -3
View File
@@ -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(() => {
+6
View File
@@ -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);
+4
View File
@@ -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."
}
}
+4
View File
@@ -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."
}
}
+4
View File
@@ -1186,5 +1186,9 @@
"add": "Добавлено",
"improve": "Улучшено",
"fix": "Исправлено"
},
"pwa": {
"appDescription": "Универсальный набор IP-инструментов",
"installDescription": "Вы открываете IPCheck.ing уже в {count}-й раз — установите его как приложение, чтобы пользоваться им быстрее и удобнее."
}
}
+4
View File
@@ -1186,5 +1186,9 @@
"add": "新增",
"improve": "优化",
"fix": "修复"
},
"pwa": {
"appDescription": "一站式 IP 工具箱",
"installDescription": "这已经是你第 {count} 次使用 IPCheck.ing 了,把它安装为应用,可以获得更好的使用体验,打开也更方便。"
}
}
+26 -6
View File
@@ -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
View File
@@ -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);
});
});