Skip to content

fix: use legacy Safari push path only for existing legacy subscribers#1441

Merged
sherwinski merged 18 commits intomainfrom
fix/safari-vapid-push-detection
Mar 31, 2026
Merged

fix: use legacy Safari push path only for existing legacy subscribers#1441
sherwinski merged 18 commits intomainfrom
fix/safari-vapid-push-detection

Conversation

@sherwinski
Copy link
Copy Markdown
Contributor

@sherwinski sherwinski commented Mar 20, 2026

Description

1 Line Summary

Fix Safari 16.4+ to use VAPID push for new users instead of the broken legacy path, while preserving legacy push for existing subscribers and Safari < 16.4.

Details

useSafariLegacyPush() previously returned true whenever window.safari.pushNotification existed, forcing ALL macOS Safari users onto the legacy push path — including Safari 16.4+ which fully supports standard Web Push (VAPID). This caused new users on modern Safari to hit "Safari url/icon/certificate invalid" errors.

Now the function checks the actual subscription state:

  • Safari 16.4+: Only users with an active legacy subscription (permission === 'granted' with a valid deviceToken) stay on the legacy path. New users go through VAPID.
  • Safari < 16.4: PushSubscriptionOptions is unavailable, so these browsers are routed to the legacy path regardless of subscription state.
Scenario Before After
New user on Safari 16.4+ Legacy path (broken) VAPID path
Existing legacy subscriber on Safari 16.4+ Legacy path Legacy path (preserved)
Any user on Safari < 16.4 Legacy path Legacy path
Chrome Unaffected Unaffected

Systems Affected

  • WebSDK
  • Backend
  • Dashboard

Validation

Tests

Info

12 new unit tests covering useSafariLegacyPush() and getSubscriptionType() across Safari < 16.4, Safari 16.4+ new users, and existing legacy subscribers.

Manual testing performed on:

  • Safari 15.6 (BrowserStack Local): New user subscribes via legacy path successfully
  • Safari 26.2 (local): New user with clean profile gets VAPID slidedown, subscribes with web.push.apple.com token, type = SafariPush
  • Safari 26.2 (local): Existing legacy subscriber (created on main) correctly detected and kept on legacy path with deviceToken preserved
  • Chrome 146 (local): No regression, standard VAPID flow works

Checklist

  • All the automated tests pass or I explained why that is not possible
  • I have personally tested this on my machine or explained why that is not possible
  • I have included test coverage for these changes or explained why they are not needed

Programming Checklist
Interfaces:

  • Don't use default export
  • New interfaces are in model files

Functions:

  • Don't use default export
  • All function signatures have return types
  • Helpers should not access any data but rather be given the data to operate on.

Typescript:

  • No Typescript warnings
  • Avoid silencing null/undefined warnings with the exclamation point

Other:

  • Iteration: refrain from using elem of array syntax. Prefer forEach or use map
  • Avoid using global OneSignal accessor for context if possible. Instead, we can pass it to function/constructor so that we don't call OneSignal.context

Screenshots

Info

Safari 15.6 — new user, legacy path (BrowserStack)

Screenshot 2026-03-30 at 11 02 40 PM

Safari 26.2 — new user, VAPID path

Screenshot 2026-03-31 at 11 51 29 AM

Safari 26.2 — existing legacy subscriber preserved

Screenshot 2026-03-31 at 11 49 13 AM

Chrome 146 — no regression

Screenshot 2026-03-31 at 11 49 34 AM

Checklist

  • I have included screenshots/recordings of the intended results or explained why they are not needed

@sherwinski sherwinski force-pushed the fix/safari-vapid-push-detection branch from 2c6fed0 to 034ccf0 Compare March 30, 2026 17:27
@sherwinski sherwinski marked this pull request as ready for review March 30, 2026 20:01
Comment thread src/shared/environment/detect.test.ts Outdated
Comment thread src/shared/environment/detect.ts
@sherwinski
Copy link
Copy Markdown
Contributor Author

sherwinski commented Mar 31, 2026

@fadi-george @abdulraqeeb33 Results of manually-tested scenarios:

Scenario Browser Path Taken Result
New user Safari 15.6 (< 16.4) Legacy (no VAPID available) Pass
New user (clean slate) Safari 26.2 (16.4+) VAPID Pass
Existing legacy subscriber Safari 26.2 (16.4+) Legacy (preserved) Pass
Existing subscriber Chrome 146 VAPID (Chrome) Pass

Safari 16.4+ supports standard Web Push (VAPID), but
`window.safari.pushNotification` was never removed. The SDK checked
for API existence to decide the push path, which routed ALL macOS
Safari users through the legacy flow — even on versions that fully
support VAPID. When the legacy push package validation fails (e.g.
invalid allowedDomains), this causes the error:
"Safari url/icon/certificate invalid or in private mode"

Now `useSafariLegacyPush()` checks the actual permission state: only
users who have already granted legacy push (with a valid deviceToken)
stay on the legacy path. New users on Safari 16.4+ are routed through
the standard VAPID flow via `useSafariVapidPush()`.
Cover the new `useSafariLegacyPush()` behavior:
- Returns false when window.safari is undefined
- Returns false when pushNotification API exists but no safariWebId
- Returns false for new users (permission "default", no deviceToken)
- Returns false for denied users
- Returns true only for existing legacy subscribers (granted + deviceToken)

Also tests `getSubscriptionType()` integration to verify legacy
subscribers stay on SafariLegacyPush while new users are not routed
to the legacy path.
For Safari < 16.4 where VAPID isn't available, returns  early so users are routed through the legacy Safari push path. This is the only path that works on those browsers. On Safari 16.4+ the existing logic still applies: only users with an active legacy subscription (granted + deviceToken) use the legacy path.
Existing tests now explicitly mock VAPID support in beforeEach to simulate Safari 16.4+
Adds two new test cases for Safari < 16.4:
- useSafariLegacyPush returns true even for new users when VAPID isn't available
- getSubscriptionType returns _SafariLegacyPush when VAPID isn't available
@sherwinski sherwinski force-pushed the fix/safari-vapid-push-detection branch from 2da22a5 to 12b5a32 Compare March 31, 2026 15:24
@sherwinski
Copy link
Copy Markdown
Contributor Author

@claude[agent] review

@Claude
Copy link
Copy Markdown

Claude AI commented Mar 31, 2026

@claude[agent] review

Code review complete. The implementation correctly fixes Safari 16.4+ VAPID routing by checking permission state instead of just API presence. Test coverage is comprehensive (10 new tests), manual testing validates all scenarios, and backward compatibility is preserved for existing legacy subscribers. Minor suggestion: consider reusing supportsVapidPush at detect.ts:41-43 to eliminate duplication. Overall: LGTM ✅

@Claude Claude AI changed the title Fix/safari vapid push detection Addressing PR comments Mar 31, 2026
@sherwinski sherwinski changed the title Addressing PR comments fix: use legacy Safari push path only for existing legacy subscribers Mar 31, 2026
Comment thread src/shared/environment/detect.test.ts
};
};

const clearVapidSupport = () => {
Copy link
Copy Markdown
Contributor

@fadi-george fadi-george Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesnt seem needed, tests pass without it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I get:

FAIL  src/shared/environment/detect.test.ts > getSubscriptionType > returns SafariLegacyPush on Safari < 16.4 (no VAPID support)
AssertionError: expected 'ChromePush' to be 'SafariLegacyPush' // Object.is equality

Expected: "SafariLegacyPush"
Received: "ChromePush"

Comment thread src/shared/environment/detect.test.ts Outdated
Comment thread package.json Outdated
"prepare": "vp config"
},
"dependencies": {
"@types/jest": "^30.0.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

Copy link
Copy Markdown
Contributor Author

@sherwinski sherwinski Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the time it was needed for changes to the test files; specifically for importing afterEach and beforeEach

Copy link
Copy Markdown
Contributor

@fadi-george fadi-george Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean, that comes from vitest

Copy link
Copy Markdown
Contributor

@fadi-george fadi-george Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran build,test, & lint without it and didnt get any errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoot, I didn't realize this was added as a dependency. Removing

@fadi-george
Copy link
Copy Markdown
Contributor

fadi-george commented Mar 31, 2026

but even legacy safari subscriptions on 16.4+ can be migrated, see
#1329
but could be a follow up pr

Comment thread vite.config.ts
@@ -150,10 +150,10 @@ export default defineConfig({
server: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe could do:

const getBooleanEnv = (env?: string) => {
  return !!env && env === 'true';
};

Comment thread vite.config.ts Outdated
@abdulraqeeb33
Copy link
Copy Markdown

@fadi-george @abdulraqeeb33 Results of manually-tested scenarios:

Scenario Browser Path Taken Result
New user Safari 15.6 (< 16.4) Legacy (no VAPID available) Pass
New user (clean slate) Safari 26.2 (16.4+) VAPID Pass
Existing legacy subscriber Safari 26.2 (16.4+) Legacy (preserved) Pass
Existing subscriber Chrome 146 VAPID (Chrome) Pass

do we have to test on any other browser?
do we claim we support this on other browsers?

@sherwinski
Copy link
Copy Markdown
Contributor Author

but even legacy safari subscriptions on 16.4+ can be migrated, see #1329 but could be a follow up pr

Agreed, let's have the next release address:

  • token migration from legacy to VAPID
  • drop support for legacy subscriptions

@sherwinski
Copy link
Copy Markdown
Contributor Author

@fadi-george @abdulraqeeb33 Results of manually-tested scenarios:
Scenario Browser Path Taken Result
New user Safari 15.6 (< 16.4) Legacy (no VAPID available) Pass
New user (clean slate) Safari 26.2 (16.4+) VAPID Pass
Existing legacy subscriber Safari 26.2 (16.4+) Legacy (preserved) Pass
Existing subscriber Chrome 146 VAPID (Chrome) Pass

do we have to test on any other browser? do we claim we support this on other browsers?

I included a test for Chrome to show there were no regressions. The issue that this PR addresses only affects Safari.

@sherwinski sherwinski requested a review from fadi-george March 31, 2026 20:46
Comment thread tsconfig.json Outdated
@sherwinski sherwinski requested a review from fadi-george March 31, 2026 20:55
const safariWebId = OneSignal?.config?.safariWebId;
if (!safariWebId) return false;

const hasVapidSupport =
Copy link
Copy Markdown
Contributor

@fadi-george fadi-george Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already do this in supportsVapidPush so could do:

export const supportsVapidPush =
  typeof PushSubscriptionOptions !== 'undefined' &&
  // eslint-disable-next-line no-prototype-builtins
  PushSubscriptionOptions.prototype.hasOwnProperty('applicationServerKey');



export const useSafariLegacyPush = (): boolean => {
  if (!isBrowser() || window.safari?.pushNotification == undefined) {
    return false;
  }
  const safariWebId = OneSignal?.config?.safariWebId;
  if (!safariWebId) return false;

  if (!supportsVapidPush) return true;

  const { permission, deviceToken } = window.safari.pushNotification.permission(safariWebId);
  return permission === 'granted' && deviceToken != null;
};

@sherwinski sherwinski merged commit 6bfcc8b into main Mar 31, 2026
3 checks passed
@sherwinski sherwinski deleted the fix/safari-vapid-push-detection branch March 31, 2026 22:26
@github-actions github-actions Bot mentioned this pull request Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants