Do it yourself
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main pageChatGPT & Codex
Push is the native feature App Review notices first, and most of the work lives in your web app and backend, which makes it a good Codex task. WebViewGold already ships the OneSignal SDK in both native projects; Codex builds the opt-in moment, syncs each device's subscription ID to your server and writes the endpoint that sends targeted notifications. Here is the split, the AGENTS.md rules and a prompt that avoids the classic mistakes.
From $109 one-time per platform, or let our team do it all from $698.
The goal is user-specific push: "New reply to your comment" on one person's phone, not a broadcast to everyone who installed the app. That takes three pieces: the native app registered with OneSignal, your server knowing which OneSignal subscription belongs to which user, and a send call that opens the right screen inside the app when someone taps the notification.
The reviewer sees less of this than you might think, but they see it early. A permission dialog that pops up before the first screen has rendered reads like a website grabbing permissions without a reason. Apple's Guideline 4.5.4 also says push must not be required for the app to work, and promotional pushes need explicit opt-in in your own UI plus a way to opt out. So: ask after a meaningful action, explain what people will get, and keep a switch in settings. Done that way, push is one of the clearest signals of native functionality under Guideline 4.2 (what 4.2 expects from web-based apps).
This guide assumes a React front end, an Express API and Firestore, with the user agent helper isNativeApp() from the WebViewGold AGENTS.md template. The pattern transfers to any stack.
| Piece | Where it lives | Who |
|---|---|---|
| OneSignal app with APNs (iOS) and FCM (Android) credentials | OneSignal dashboard | You, once |
| OneSignal switched on in the native apps | iOS: kPushEnabled true and oneSignalID in Config.swift. Android: PUSH_ENABLED true in Config.java, App ID in ONE_SIGNAL_APP_ID in build.gradle | You, or Codex in the native project |
| First-run prompt off | askforpushpermissionatfirstrun false (Config.swift), requirePush false (Config.java) | You |
| Opt-in sheet that triggers the native dialog | Web app, navigating to registerpush:// | Codex |
| Subscription ID to your server | getonesignalplayerid:// fills onesignalplayerid; or turn on autoInjectVariable / AUTO_INJECT_VARIABLES; or receive ?onesignal_push_id= on the first URL via kPushEnhanceUrl / PUSH_ENHANCE_WEBVIEW_URL | Codex (web and Express) |
| Status in settings | checkNativePushPermissions:// fills nativePushEnabled; settingsapp:// opens the app's system settings | Codex |
| Sending | Express calls the OneSignal REST API | Codex, reviewed by you |
Append this to your AGENTS.md. Codex reads it at the start of every session, and the Review guidelines heading is what Codex code review follows when you comment @codex review on a pull request. Every scheme and key is taken from the iOS push docs and the Android push docs.
## Push notifications (OneSignal via WebViewGold)
- Native side: OneSignal is enabled in the WebViewGold projects
(iOS: kPushEnabled + oneSignalID; Android: PUSH_ENABLED + ONE_SIGNAL_APP_ID).
The first-run prompt is OFF (askforpushpermissionatfirstrun / requirePush = false).
- Never add the OneSignal Web SDK, a web push service worker or
Notification.requestPermission(). In the app, permission is requested only by
navigating to registerpush://, always behind isNativeApp().
- Permission status: checkNativePushPermissions:// sets window.nativePushEnabled
(asynchronously). If false, offer a button that navigates to settingsapp://.
- Device identity: getonesignalplayerid:// sets window.onesignalplayerid
(asynchronously; may be empty before the user allows push). Poll with a timeout.
On iOS, combine data calls: multiapicall://getonesignalplayerid,getappversion
- Storage: pushDevices/{subscriptionId} = { uid, platform, updatedAt }.
One user can have many devices. Delete the row on logout.
- Sending happens only on the server (server/push/send.js), with ONESIGNAL_API_KEY
from the environment. Deep links go in data.url (opens inside the app).
Never set OneSignal's top-level url field: that is the Launch URL and opens the browser.
- Promotional pushes need explicit opt-in in our UI and an opt-out in settings.
## Review guidelines
- P1: ONESIGNAL_API_KEY, or any call to api.onesignal.com, in client code.
- P1: a notification payload with a top-level url instead of data.url.
- P1: registerpush:// or getonesignalplayerid:// without an isNativeApp() check.
WebViewGold has allowed us the shortest and most efficient time-to-market ever possible from among all the other app platforms we have ever tried during the past 5 years. I don’t know why I hadn’t discovered you sooner, but I am really, really glad I did. Currently nearing 6000 app downloads on both iOS App Store & Google Play Store without any problem!
Start Codex in the repository with workspace write access and approvals on request: codex --sandbox workspace-write --ask-for-approval on-request. In that mode the network is off by default, which suits this task: it should produce code and mocked tests, not live notifications. Replace the bracketed parts.
Read AGENTS.md first, especially the WebViewGold bridge and the push section. Then implement push notifications end to end in this repository. Do not touch the native projects.
Context: the native iOS and Android apps use OneSignal through WebViewGold. The first-run permission prompt is switched off (askforpushpermissionatfirstrun = false in Config.swift, requirePush = false in Config.java), so our web app decides when to ask.
1. Opt-in: after the user [creates their first project], show a short sheet explaining which notifications they will get ([replies, reminders]), with Enable and Not now. Enable navigates to registerpush:// when isNativeApp() is true. Remember Not now for 14 days. Never show the sheet in browsers.
2. Subscription sync: after login and after the opt-in, navigate to getonesignalplayerid:// and wait for window.onesignalplayerid (poll every 200 ms, give up after 5 s, ignore empty values). POST it with the platform to /api/push/devices, authenticated with the Firebase ID token. The server stores pushDevices/{subscriptionId} = { uid, platform, updatedAt }, so a phone that changes hands moves to the new user. On logout, call DELETE /api/push/devices/:id before signing out.
3. Settings: a Notifications row. Navigate to checkNativePushPermissions:// and read window.nativePushEnabled after a short delay. If it is false, show "Notifications are off" and a button that navigates to settingsapp://.
4. Sending: server/push/send.js exporting sendPushToUser(uid, { title, body, path }). It loads the user's pushDevices, calls the OneSignal REST API with include_subscription_ids, and puts the deep link (our production origin + path) in data.url. Never use the top-level url field. Read ONESIGNAL_APP_ID and ONESIGNAL_API_KEY from the environment and add both to .env.example without values. Call it from [the new-reply handler].
5. Tests: unit tests for the device routes (auth required, upsert, delete only own rows) and for sendPushToUser with fetch mocked. Do not call the real OneSignal API.
6. Finish with: files changed, environment variables I must set, and a numbered device test list.Use this as the yardstick when you read the diff. Two lines matter most: the API key never leaves the server, and the deep link sits in data.url. OneSignal's top-level url is the Launch URL, which opens a browser; the WebViewGold docs explicitly ask for the additional data field url instead.
// server/push/index.js (Express + firebase-admin, Node 18+ for global fetch)
import express from "express";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
import { requireUser } from "../auth/requireUser.js"; // verifies the Firebase ID token, sets req.uid
const db = getFirestore();
export const pushRouter = express.Router();
// One row per device. The document ID is the OneSignal subscription ID, so a phone
// that changes hands (logout, login as someone else) simply moves to the new user.
pushRouter.post("/api/push/devices", requireUser, express.json(), async (req, res) => {
const { subscriptionId, platform } = req.body || {};
if (!subscriptionId) return res.status(400).json({ error: "subscriptionId required" });
await db.collection("pushDevices").doc(subscriptionId).set({
uid: req.uid, platform: platform || "unknown", updatedAt: FieldValue.serverTimestamp()
});
res.status(204).end();
});
pushRouter.delete("/api/push/devices/:id", requireUser, async (req, res) => {
const ref = db.collection("pushDevices").doc(req.params.id);
const snap = await ref.get();
if (snap.exists && snap.get("uid") === req.uid) await ref.delete();
res.status(204).end();
});
export async function sendPushToUser(uid, { title, body, path }) {
const devices = await db.collection("pushDevices").where("uid", "==", uid).get();
const ids = devices.docs.map((d) => d.id);
if (ids.length === 0) return 0;
const res = await fetch("https://api.onesignal.com/notifications?c=push", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Key " + process.env.ONESIGNAL_API_KEY // server only
},
body: JSON.stringify({
app_id: process.env.ONESIGNAL_APP_ID,
target_channel: "push",
include_subscription_ids: ids,
headings: { en: title },
contents: { en: body },
// WebViewGold opens this inside the app. Not the top-level "url" (Launch URL).
data: { url: "https://app.example.com" + path }
})
});
if (!res.ok) throw new Error("OneSignal " + res.status + ": " + (await res.text()));
return ids.length;
}Plan an afternoon; most of it is waiting for builds.
Add the Apple iOS platform with your APNs credentials and the Google Android platform with the Firebase service account JSON that OneSignal asks for. Copy the App ID and the App API key. The key goes into your server's environment, never into AGENTS.md, a cloud task environment you share, or the client bundle.
iOS: kPushEnabled true, your App ID in oneSignalID, askforpushpermissionatfirstrun false, and the Push Notifications capability enabled for your bundle ID. Android: PUSH_ENABLED true, the App ID in ONE_SIGNAL_APP_ID in build.gradle, requirePush false. Config.swift and Config.java are plain text, so a second Codex session in the WebViewGold project can set exactly these values; signing stays with you.
Paste the prompt above. Before you accept the result, type /review in the CLI for a second pass over the uncommitted changes, then read git diff yourself with the AGENTS.md rules next to it. Look for the two P1 rules first.
Set ONESIGNAL_APP_ID and ONESIGNAL_API_KEY on the server and deploy the web app and API. The native projects do not change for this step, which is the nice part of keeping logic on the web side.
TestFlight for iOS, an internal testing track for Android. Push needs a signed build on hardware. No Mac at hand? The Cloud Builder sets the same WebViewGold options in a browser form and builds both apps.
First from the OneSignal dashboard with an additional data field url, which proves the native side. Then trigger your own event (a reply, a reminder), which proves the server, the device mapping and the deep link.
settingsapp://.nativePushEnabled is only true when both the notification setting and the runtime permission are granted.Notification.requestPermission(). Web push is the wrong channel inside the app; WebViewGold already ships the native SDK.window.onesignalplayerid right after the navigation. Injected variables arrive asynchronously, so poll with a timeout.multiapicall://getonesignalplayerid,getappversion (see the MultiAPICall docs).url field, which sends users to the browser.registerpush:// while no push service is enabled in the native config. Nothing happens, and the button looks broken.Yes. WebViewGold supports OneSignal, Firebase and Pushwoosh. For Firebase, switch on kFirebasePushEnabled (iOS) or FIREBASE_PUSH_ENABLED (Android), add the Google service files, and read the token with getfirebaseplayerid://, which fills firebaseplayerid. The AGENTS.md rules stay the same; only the variable names and the send API change.
Not end to end. A push needs a signed build on a real device and live credentials. Codex can unit-test the device routes and the send function with fetch mocked, which catches most bugs; the last step is your device test.
Because the WebViewGold bridge hands your web app the subscription ID (getonesignalplayerid:// or the injected variable), so that is the identifier you can rely on. Mapping subscription IDs to your own user ID also makes logout, account switching and account deletion easy to get right.
Only if push is the core of the app from the first second, for example an alerting app. For most apps, asking after a meaningful action gets more people to say yes and looks less like a website asking for permissions.
It helps, but no single feature guarantees approval. Reviewers judge the whole experience: a clean mobile layout, app-like navigation, offline handling and native touches such as push and sharing.
Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main page$349 per license, one-time add-on
No Mac, no Xcode, no Android Studio: configure in the browser, we build both apps in the cloud and hand them to App Store Connect and the Play Console.
How the Cloud Builder works Try the free demo$698 per platform $798
Send us your URL and icon. Our team configures, builds and delivers your app within 72 hours, white-label, with source projects. Both platforms for $1,158.
Full Service details Powered byWinter Sale prices valid until December 31, 2026.
App Store & Google Play approval guarantee – or your money back within 14 days
Rather hand it over? Full Service: we configure, build and deliver within 72 hours, from $698 per platform. Questions? Chat with us 24/7.
Refunds are granted under the conditions listed in our FAQ: a non-content rejection by Apple and/or Google, claimed within 14 days after purchase.