feat(notifications): enable() asks through Puter's prompt after a revoke

When the user turned an app's notifications off in Puter, the browser's
permission on the app's origin no longer re-enables them. enable() asks
again through Puter's permission prompt and retries once.
This commit is contained in:
Daniel Salazar committed 2026-10-28 01:17:00 -07:00
1 parent 1aa7caf44c
commit 1b6de39a13
3 files changed
+74 -4

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ platforms: [websites, apps]
Turns on push notifications for your app in the browser the user is in. Call it from a click: it asks the browser's permission first, before anything else, because browsers only show that prompt from a user gesture.
On a website, the browser asks on your own origin, and pushes arrive through your service worker (see [`onPush()`](/Notifications/onPush/)). Puter then sends a confirmation push to that browser and only keeps the subscription once your page hands it back, so a subscription can only come from a browser where the user said yes. Allowing it also lets your app's notifications go out as pushes, which the user can revoke in Puter's settings.
On a website, the browser asks on your own origin, and pushes arrive through your service worker (see [`onPush()`](/Notifications/onPush/)). Puter then sends a confirmation push to that browser and only keeps the subscription once your page hands it back, so a subscription can only come from a browser where the user said yes. Allowing it also lets your app's notifications go out as pushes. If the user later turns your app off in Puter's settings, that stands: `enable()` then asks again through Puter's permission prompt rather than relying on the browser's permission.
Inside the Puter desktop it does what [`enablePush()`](/Notifications/enablePush/) does.
@@ -27,8 +27,14 @@ export async function enable(options = {}) {
if ((await asking) !== 'granted') return false;
if (needsSignIn(puter)) await signInVisitor(puter);
await syncWebPush(puter, {
worker: workerFor(puter.env, options.serviceWorker),
});
const worker = workerFor(puter.env, options.serviceWorker);
try {
await syncWebPush(puter, { worker });
} catch (error) {
// The user turned this app off in Puter; only Puter's prompt undoes it.
if (error?.code !== 'push_revoked') throw error;
if (!(await this.requestPermission())) return false;
await syncWebPush(puter, { worker });
}
return true;
}
@@ -251,3 +251,67 @@ describe('onPush', () => {
expect(module.onNotification).toHaveBeenCalledTimes(1);
});
});
describe('enable', () => {
const withBrowser = (nav, permission = 'granted') => {
globalThis.PushManager = function PushManager() {};
globalThis.Notification = {
permission,
requestPermission: vi.fn(async () => 'granted'),
};
Object.defineProperty(globalThis.navigator, 'serviceWorker', {
value: nav.serviceWorker,
configurable: true,
});
};
afterEach(() => {
delete globalThis.PushManager;
delete globalThis.Notification;
});
const makeModule = () =>
new NotificationsModule({
...puter,
onAuthStateChanged: () => () => {},
});
it('asks the browser before anything is awaited', async () => {
const nav = makeNav({ existing: makeSubscription() });
withBrowser(nav, 'default');
mockRequest.mockImplementation(async (_p, method, route) =>
route === '/push/web/key'
? { publicKey: KEY }
: { status: 'active', device: { uid: 'd1' } },
);
const pending = makeModule().enable();
expect(Notification.requestPermission).toHaveBeenCalledTimes(1);
await expect(pending).resolves.toBe(true);
});
it('asks through Puter`s prompt when the user turned the app off there', async () => {
const nav = makeNav({ existing: makeSubscription() });
withBrowser(nav);
let revoked = true;
mockRequest.mockImplementation(async (_p, method, route) => {
if (route === '/push/web/key') return { publicKey: KEY };
if (revoked) {
const { PuterJSError } =
await import('../../lib/PuterJSError.js');
throw new PuterJSError('off', 'push_revoked');
}
return { status: 'active', device: { uid: 'd1' } };
});
const module = makeModule();
module.requestPermission = vi.fn(async () => {
revoked = false;
return true;
});
await expect(module.enable()).resolves.toBe(true);
expect(module.requestPermission).toHaveBeenCalledTimes(1);
revoked = true;
module.requestPermission = vi.fn(async () => false);
await expect(module.enable()).resolves.toBe(false);
});
});