Skip to content
WebViewGold Vibe Coding to App Store
English
Get WebViewGold

Claude Code + push

Add OneSignal push notifications to a Claude Code web app with WebViewGold

Push is the native feature reviewers notice first and the one users actually keep. With WebViewGold, OneSignal runs inside the native shell; your web app only decides when to ask, which user a device belongs to and which screen a notification opens. Claude Code can write all of that in one session, and this guide gives you the prompt, the server code and the test plan.

From $109 one-time per platform, or let our team do it all from $698.

  • Approval guarantee – or money back within 14 days
  • No coding needed
  • One-time payment, free updates
  • 24/7 human support
  • Made in Germany since 2014 · ISO 27001

What you are wiring together, and why reviewers care

WebViewGold ships the OneSignal SDK inside both native projects. Once you switch it on, the app receives pushes even while it is closed. Your web app stays in charge of three decisions: when the system permission prompt appears, which user a device belongs to, and which URL a notification opens. Each of those is a URL scheme or an injected JavaScript variable, documented in the iOS push docs and the Android push docs.

Reviewers care for two reasons. Guideline 4.2 expects a web-based app to do more than a website, and a working push opt-in is usually the first native capability they see. Guideline 4.5.4 sets the limits: the app must work without push, notifications must not carry sensitive personal data, and promotional pushes need explicit opt-in in your UI plus a way to opt out. Android 13 and newer add a runtime permission (POST_NOTIFICATIONS), so the timing of your prompt matters on both platforms.

The flow you will end up with: after onboarding, the web app navigates to registerpush://; once the user allows, it reads onesignalplayerid and stores it on the user; your server sends a push to that subscription ID with a url in the additional data, and WebViewGold opens that URL when the notification is tapped. For the overall picture of native features, see the push notifications hub.

What Claude Code does and what you do

AreaClaude CodeYou
OneSignal dashboardNothing; it has no access to your dashboardCreate the app, add the APNs credentials (iOS) and the Firebase credentials (Android), copy the App ID and create a REST API key
WebViewGold projectsCan set kPushEnabled, oneSignalID, PUSH_ENABLED and the build.gradle ID if the projects live in the repository; you review the diffCapabilities in the Apple Developer portal, the OneSignal extension bundle ID, signing and builds
Web appPush helper, opt-in screen, settings row, register route, send helper, database migration, CLAUDE.md rulesApprove the plan, set the environment variables, deploy
TestingA written test plan and, on request, a hidden debug page that shows the push status and subscription IDA real iPhone and a real Android phone, TestFlight and internal testing

Switch on OneSignal and let Claude Code do the web part

Allow about an hour, most of it in the Apple and Firebase consoles.

  1. Create one OneSignal app for both platforms

    Add iOS with your APNs credentials (OneSignal recommends a .p8 auth key; a push certificate also works) and Android with the Firebase service account JSON that OneSignal asks for. Copy the OneSignal App ID and create a REST API key for your server.

  2. iOS: Config.swift and capabilities

    Set kPushEnabled to true, paste the App ID into oneSignalID and set askforpushpermissionatfirstrun to false so the prompt is yours to time. In the Apple Developer portal, enable Push Notifications and App Groups for the App ID. The OneSignal extension target needs your bundle ID plus .OneSignalNotificationServiceExtension.

  3. Android: Config.java and build.gradle

    Set PUSH_ENABLED to true, paste the App ID into ONE_SIGNAL_APP_ID in build.gradle (Module: app) and set requirePush to false. Replace ic_stat_onesignal_default.png with your own monochrome icon, otherwise users see a generic bell.

  4. Mark the app in the user agent

    Set useragent_iphone and useragent_ipad (iOS) and USER_AGENT (Android) to a normal mobile user agent followed by WebViewGoldApp, so isNativeApp() can tell the app from Safari or Chrome. The CLAUDE.md template explains the convention.

  5. Run the prompt in plan mode

    Press Shift+Tab until Claude Code shows plan mode (or start it with claude --permission-mode plan), paste the prompt below and approve the plan only when it lists the files and the table you expect. Then let it implement, read the diff and deploy.

  6. Build and install

    Archive the iOS app for TestFlight and upload the Android bundle to internal testing. If you would rather not install Xcode and Android Studio for this, the Cloud Builder builds both apps in the browser and uploads them for you.

Great way of converting your web app to a native app! Fast turn-around, and a great way to get your apps into the app stores without the need to develop a full native app!

Kevin Joling · FlightConnections & TrainConnections · Trustpilot

The Claude Code prompt

Adjust the table name, the routes and the example notifications to your app. The prompt tells the agent where the authoritative scheme names live so it does not invent any.

Paste into Claude Code (plan mode first)
Our web app also runs inside native iOS and Android apps built with WebViewGold. OneSignal push is switched on in the native projects. The web app has to ask for permission at the right moment, report the device subscription ID to our server, and the server sends targeted notifications. Before writing code, read the Push Notifications sections for iOS and Android in https://www.webviewgold.com/llms-full.txt and use only URL schemes and variables documented there. Start with a plan and wait for my approval.

Implement:
1. src/lib/native.ts (reuse it if it exists): isNativeApp() checks navigator.userAgent for the token WebViewGoldApp; callNative(url) sets window.location.href only inside the app; readInjected(name, timeoutMs) polls window[name] every 100 ms and rejects after the timeout.
2. src/lib/push.ts: askForPush() navigates to registerpush://. getPushStatus() navigates to checkNativePushPermissions:// and resolves window.nativePushEnabled (true or false). syncPushId() navigates to getonesignalplayerid://, reads window.onesignalplayerid and POSTs it with the platform (ios or android, from the user agent) to /api/push/register. Never trigger two schemes in the same script run: on iOS several window.location.href calls in one script do not work, so sequence them with a delay of about one second.
3. An opt-in screen shown once after onboarding, never on the first screen, only when isNativeApp() is true: one sentence on what we send (order updates and replies), an Enable notifications button (askForPush, then syncPushId after 3 seconds) and a Not now link that hides the screen for 14 days via localStorage.
4. Settings: a Notifications row that shows the status from getPushStatus(). When it is off, show Open settings, which navigates to settingsapp://. Add a separate Product news toggle, off by default and stored on the user profile; only users who switched it on may receive promotional pushes (Apple guideline 4.5.4).
5. On every app start of a signed-in user inside the app, call syncPushId() once, because the subscription ID can change after a reinstall.
6. Server: POST /api/push/register (authenticated; upserts user_id, subscription_id, platform, last_seen_at into a push_devices table; add the migration) and a server-only helper sendPushToUser(userId, title, message, path) that calls the OneSignal REST API with include_subscription_ids and puts the full https URL of the target screen into data.url. Read ONESIGNAL_APP_ID and ONESIGNAL_REST_API_KEY from server environment variables only.
7. Call sendPushToUser where a new reply or an order status change is created. No personal or sensitive details in the notification text.
8. Add the push rules above to CLAUDE.md.

Report the files you changed, the migration, the environment variables I must set and anything you could not verify.

The register route and the send helper

Next.js App Router with TypeScript; ask the agent to adapt it to Express, a Supabase Edge Function or a Cloud Function if that is your stack. The data.url value is what WebViewGold opens when the notification is tapped; OneSignal's Launch URL field is not the one to use.

typescript
// app/api/push/register/route.ts  (called by the web app inside the native shell)
import { NextResponse } from "next/server";
import { getUser, db } from "@/lib/server"; // your auth + database helpers

export async function POST(req: Request) {
  const user = await getUser(req);
  if (!user) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const { subscriptionId, platform } = await req.json();
  if (typeof subscriptionId !== "string" || subscriptionId.length < 10) {
    return NextResponse.json({ error: "invalid subscription id" }, { status: 400 });
  }
  await db.pushDevices.upsert({ userId: user.id, subscriptionId, platform, lastSeenAt: new Date() });
  return NextResponse.json({ ok: true });
}

// lib/push-server.ts  (server only, never import from client components)
export async function sendPushToUser(userId: string, title: string, message: string, path: string) {
  const devices = await db.pushDevices.findMany({ userId });
  if (devices.length === 0) return { sent: 0 };

  const res = await fetch("https://api.onesignal.com/notifications?c=push", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Key " + process.env.ONESIGNAL_REST_API_KEY,
    },
    body: JSON.stringify({
      app_id: process.env.ONESIGNAL_APP_ID,
      include_subscription_ids: devices.map((d) => d.subscriptionId),
      headings: { en: title },
      contents: { en: message },
      data: { url: "https://app.example.com" + path }, // opened by WebViewGold on tap
    }),
  });
  if (!res.ok) throw new Error("OneSignal " + res.status + ": " + (await res.text()));
  return { sent: devices.length };
}

Verification checklist on real phones

Simulators and desktop browsers cannot prove any of this. Use the TestFlight build on an iPhone and the internal testing build on an Android 13+ phone, both freshly installed.

  • First launch: no system prompt appears, because askforpushpermissionatfirstrun and requirePush are false.
  • After onboarding, your opt-in screen appears; Enable notifications shows the system dialog; Not now hides the screen and it stays hidden on the next launch.
  • After allowing, a row appears in push_devices and the device shows up in OneSignal under Audience → Subscriptions.
  • Send a test from the OneSignal dashboard with Additional data url set to a deep page such as /orders/42. Tap it three times: with the app killed, in the background and in the foreground. Each tap opens that page in the app.
  • Trigger a real event (a reply to the test user) and confirm that only that user's devices receive the push.
  • On a second device, deny the permission: Settings shows Notifications are off and Open settings jumps to the system screen via settingsapp://. Re-enable there, return, and the status updates.
  • Delete and reinstall the app, sign in: a new subscription ID is registered.

Pitfalls

  • Two schemes in one script run on iOS. Setting window.location.href to several schemes in the same script does not work on iOS (Android handles it). Sequence calls with a delay, or use the documented multiapicall://getonesignalplayerid,getappversion for the supported data calls (MultiAPICall docs).
  • Launch URL instead of additional data. WebViewGold reads the url field from the additional data. Leave kPushOpenDeeplinkInBrowser (iOS) and OPEN_NOTIFICATION_URLS_IN_SYSTEM_BROWSER (Android) false if links should open inside the app.
  • Reading the ID too early. onesignalplayerid is injected asynchronously and may not exist until the SDK has registered. Poll with a timeout and retry on the next app start instead of failing silently.
  • Relying on the URL parameter. With kPushEnhanceUrl or PUSH_ENHANCE_WEBVIEW_URL, only the first request gets ?onesignal_push_id=…; a client-side router loses it. Store it in a cookie or session right away, or use autoInjectVariable / AUTO_INJECT_VARIABLES.
  • A push service that is not switched on. registerpush:// does nothing if no push service is enabled in the wrapper. Check the config before debugging your JavaScript.
  • Secrets in the client. The REST API key belongs in server environment variables only. Claude Code never needs the real value; give it the variable name.

Frequently asked questions

Can I use Firebase Cloud Messaging instead of OneSignal?

Yes. Set kFirebasePushEnabled in Config.swift with your GoogleService-Info.plist, and FIREBASE_PUSH_ENABLED in Config.java with your google-services.json. The web side is the same, except the ID comes from getfirebaseplayerid:// as firebaseplayerid.

Is push required for App Store approval?

No, and the app must not depend on it (guideline 4.5.4). It is still one of the strongest signals for guideline 4.2 that your app is more than a website.

Can I send notifications without a server?

Yes. Broadcasts from the OneSignal dashboard need no code, and local reminders scheduled with sendlocalpushmsg:// work without any push service. A server is only needed for per-user messages.

Why not ask for permission on the first launch?

iOS shows the system dialog once. If the user declines because they do not know your app yet, only the system settings can change it. Asking after a moment that explains the value converts much better.

Does web push from a service worker work inside the app?

Do not rely on it. Inside the WebViewGold app, push is native through OneSignal, Firebase or Pushwoosh. Keep web push, if you use it, for browser visitors only.

Three ways to get your app into the stores

Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.

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 page

Cloud Builder

$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

Full Service

$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 by WeSetupYourWebViewApp

Winter Sale prices valid until December 31, 2026.

Ready to ship your app?

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.