Skip to content

Commit f291489

Browse files
authored
fix: [SDK-4737] persist user consent in localStorage to survive iOS PWA wedge (#1474)
1 parent 6124325 commit f291489

5 files changed

Lines changed: 77 additions & 6 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
},
7878
{
7979
"path": "./build/releases/OneSignalSDK.page.es6.js",
80-
"limit": "42.57 kB",
80+
"limit": "42.65 kB",
8181
"gzip": true
8282
},
8383
{

src/onesignal/OneSignal.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type Bell from 'src/page/bell/Bell';
22
import { getAppConfig } from 'src/shared/config/app';
33
import type { AppConfig, AppUserConfig } from 'src/shared/config/types';
4-
import { db, dbPromise } from 'src/shared/database/client';
4+
import { dbPromise } from 'src/shared/database/client';
55
import { getConsentGiven, isConsentRequiredButNotGiven } from 'src/shared/database/config';
66
import { getSubscription } from 'src/shared/database/subscription';
77
import { windowEnvString } from 'src/shared/environment/detect';
@@ -20,6 +20,7 @@ import {
2020
import {
2121
getConsentRequired,
2222
removeLegacySubscriptionOptions,
23+
setConsentGiven as setStorageConsentGiven,
2324
setConsentRequired as setStorageConsentRequired,
2425
} from 'src/shared/helpers/localStorage';
2526
import { checkAndTriggerNotificationPermissionChanged } from 'src/shared/helpers/main';
@@ -219,7 +220,7 @@ export default class OneSignal {
219220

220221
// for quick access as to not wait for async operations / loading from DB
221222
OneSignal._consentGiven = consent;
222-
await db.put('Options', { key: 'userConsent', value: consent });
223+
setStorageConsentGiven(consent);
223224

224225
if (consent && OneSignal._pendingInit) await OneSignal._delayedInit();
225226
}

src/shared/database/config.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { beforeEach, describe, expect, test } from 'vite-plus/test';
2+
3+
import {
4+
getConsentGiven as getStorageConsentGiven,
5+
setConsentGiven as setStorageConsentGiven,
6+
} from '../helpers/localStorage';
7+
import { db } from './client';
8+
import { getConsentGiven } from './config';
9+
10+
describe('getConsentGiven', () => {
11+
beforeEach(() => {
12+
localStorage.clear();
13+
});
14+
15+
test('defaults to false when nothing is stored', async () => {
16+
expect(await getConsentGiven()).toBe(false);
17+
});
18+
19+
test('reads the value persisted in localStorage', async () => {
20+
setStorageConsentGiven(true);
21+
expect(await getConsentGiven()).toBe(true);
22+
23+
setStorageConsentGiven(false);
24+
expect(await getConsentGiven()).toBe(false);
25+
});
26+
27+
test('migrates a legacy IndexedDB value into localStorage', async () => {
28+
await db.put('Options', { key: 'userConsent', value: true });
29+
expect(getStorageConsentGiven()).toBeNull();
30+
31+
expect(await getConsentGiven()).toBe(true);
32+
expect(getStorageConsentGiven()).toBe(true);
33+
34+
// Once migrated, the value survives even if the IndexedDB row is gone --
35+
// a wedged PWA can no longer drop it.
36+
await db.delete('Options', 'userConsent');
37+
expect(await getConsentGiven()).toBe(true);
38+
});
39+
40+
test('prefers localStorage over a stale legacy IndexedDB value', async () => {
41+
await db.put('Options', { key: 'userConsent', value: true });
42+
setStorageConsentGiven(false);
43+
expect(await getConsentGiven()).toBe(false);
44+
});
45+
});

src/shared/database/config.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { getConsentRequired } from '../helpers/localStorage';
1+
import {
2+
getConsentGiven as getStorageConsentGiven,
3+
getConsentRequired,
4+
setConsentGiven as setStorageConsentGiven,
5+
} from '../helpers/localStorage';
26
import Log from '../libraries/Log';
37
import { db, getIdsValue, getOptionsValue } from './client';
48
import type { AppState } from './types';
@@ -69,9 +73,15 @@ export const setAppState = async (appState: AppState) => {
6973
});
7074
};
7175

72-
// make sure to also set OneSignal._consentGiven when updating 'userConsent'
76+
// make sure to also set OneSignal._consentGiven when updating consent
7377
export const getConsentGiven = async () => {
74-
return (await getOptionsValue<boolean>('userConsent')) ?? false;
78+
const stored = getStorageConsentGiven();
79+
if (stored !== null) return stored;
80+
81+
// Migrate consent persisted by older SDK versions that wrote it to IndexedDB.
82+
const legacy = (await getOptionsValue<boolean>('userConsent')) ?? false;
83+
setStorageConsentGiven(legacy);
84+
return legacy;
7585
};
7686

7787
export const isConsentRequiredButNotGiven = () => {

src/shared/helpers/localStorage.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const IS_OPTED_OUT = 'isOptedOut';
22
const IS_PUSH_NOTIFICATIONS_ENABLED = 'isPushNotificationsEnabled';
33
const PAGE_VIEWS = 'os_pageViews';
44
const REQUIRES_PRIVACY_CONSENT = 'requiresPrivacyConsent';
5+
const USER_CONSENT = 'userConsent';
56

67
/**
78
* Used in OneSignal initialization to dedupe local storage subscription options already being saved to IndexedDB.
@@ -22,6 +23,20 @@ export function getConsentRequired(): boolean {
2223
return localStorage.getItem(REQUIRES_PRIVACY_CONSENT) === 'true' || requiresUserPrivacyConsent;
2324
}
2425

26+
// Persisted in localStorage rather than IndexedDB: it's a privacy/legal opt-out
27+
// that isn't re-derivable from any other source, and on a wedged iOS Safari PWA
28+
// an IndexedDB write can be silently dropped, losing a revocation across reloads.
29+
export function setConsentGiven(value: boolean): void {
30+
localStorage.setItem(USER_CONSENT, value.toString());
31+
}
32+
33+
// Returns null when no value has been stored, so callers can fall back to the
34+
// legacy IndexedDB row for one-time migration.
35+
export function getConsentGiven(): boolean | null {
36+
const value = localStorage.getItem(USER_CONSENT);
37+
return value === null ? null : value === 'true';
38+
}
39+
2540
export function setLocalPageViewCount(count: number): void {
2641
localStorage.setItem(PAGE_VIEWS, count.toString());
2742
}

0 commit comments

Comments
 (0)