Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
},
{
"path": "./build/releases/OneSignalSDK.page.es6.js",
"limit": "42.57 kB",
"limit": "42.65 kB",
"gzip": true
},
{
Expand Down
5 changes: 3 additions & 2 deletions src/onesignal/OneSignal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type Bell from 'src/page/bell/Bell';
import { getAppConfig } from 'src/shared/config/app';
import type { AppConfig, AppUserConfig } from 'src/shared/config/types';
import { db, dbPromise } from 'src/shared/database/client';
import { dbPromise } from 'src/shared/database/client';
import { getConsentGiven, isConsentRequiredButNotGiven } from 'src/shared/database/config';
import { getSubscription } from 'src/shared/database/subscription';
import { windowEnvString } from 'src/shared/environment/detect';
Expand All @@ -20,6 +20,7 @@ import {
import {
getConsentRequired,
removeLegacySubscriptionOptions,
setConsentGiven as setStorageConsentGiven,
setConsentRequired as setStorageConsentRequired,
} from 'src/shared/helpers/localStorage';
import { checkAndTriggerNotificationPermissionChanged } from 'src/shared/helpers/main';
Expand Down Expand Up @@ -219,7 +220,7 @@ export default class OneSignal {

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

if (consent && OneSignal._pendingInit) await OneSignal._delayedInit();
}
Expand Down
45 changes: 45 additions & 0 deletions src/shared/database/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, test } from 'vite-plus/test';

import {
getConsentGiven as getStorageConsentGiven,
setConsentGiven as setStorageConsentGiven,
} from '../helpers/localStorage';
import { db } from './client';
import { getConsentGiven } from './config';

describe('getConsentGiven', () => {
beforeEach(() => {
localStorage.clear();
});

test('defaults to false when nothing is stored', async () => {
expect(await getConsentGiven()).toBe(false);
});

test('reads the value persisted in localStorage', async () => {
setStorageConsentGiven(true);
expect(await getConsentGiven()).toBe(true);

setStorageConsentGiven(false);
expect(await getConsentGiven()).toBe(false);
});

test('migrates a legacy IndexedDB value into localStorage', async () => {
await db.put('Options', { key: 'userConsent', value: true });
expect(getStorageConsentGiven()).toBeNull();

expect(await getConsentGiven()).toBe(true);
expect(getStorageConsentGiven()).toBe(true);

// Once migrated, the value survives even if the IndexedDB row is gone --
// a wedged PWA can no longer drop it.
await db.delete('Options', 'userConsent');
expect(await getConsentGiven()).toBe(true);
});

test('prefers localStorage over a stale legacy IndexedDB value', async () => {
await db.put('Options', { key: 'userConsent', value: true });
setStorageConsentGiven(false);
expect(await getConsentGiven()).toBe(false);
});
});
16 changes: 13 additions & 3 deletions src/shared/database/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { getConsentRequired } from '../helpers/localStorage';
import {
getConsentGiven as getStorageConsentGiven,
getConsentRequired,
setConsentGiven as setStorageConsentGiven,
} from '../helpers/localStorage';
import Log from '../libraries/Log';
import { db, getIdsValue, getOptionsValue } from './client';
import type { AppState } from './types';
Expand Down Expand Up @@ -69,9 +73,15 @@ export const setAppState = async (appState: AppState) => {
});
};

// make sure to also set OneSignal._consentGiven when updating 'userConsent'
// make sure to also set OneSignal._consentGiven when updating consent
export const getConsentGiven = async () => {
return (await getOptionsValue<boolean>('userConsent')) ?? false;
const stored = getStorageConsentGiven();
if (stored !== null) return stored;

// Migrate consent persisted by older SDK versions that wrote it to IndexedDB.
const legacy = (await getOptionsValue<boolean>('userConsent')) ?? false;
setStorageConsentGiven(legacy);
return legacy;
};

export const isConsentRequiredButNotGiven = () => {
Expand Down
15 changes: 15 additions & 0 deletions src/shared/helpers/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const IS_OPTED_OUT = 'isOptedOut';
const IS_PUSH_NOTIFICATIONS_ENABLED = 'isPushNotificationsEnabled';
const PAGE_VIEWS = 'os_pageViews';
const REQUIRES_PRIVACY_CONSENT = 'requiresPrivacyConsent';
const USER_CONSENT = 'userConsent';

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

// Persisted in localStorage rather than IndexedDB: it's a privacy/legal opt-out
// that isn't re-derivable from any other source, and on a wedged iOS Safari PWA
// an IndexedDB write can be silently dropped, losing a revocation across reloads.
export function setConsentGiven(value: boolean): void {
localStorage.setItem(USER_CONSENT, value.toString());
}

// Returns null when no value has been stored, so callers can fall back to the
// legacy IndexedDB row for one-time migration.
export function getConsentGiven(): boolean | null {
const value = localStorage.getItem(USER_CONSENT);
return value === null ? null : value === 'true';
}

export function setLocalPageViewCount(count: number): void {
localStorage.setItem(PAGE_VIEWS, count.toString());
}
Expand Down
Loading