自己动手
$109 每个平台,一次性付费
WebViewGold 许可证:一个现成的 Xcode 或 Android Studio 项目。粘贴您的 URL,设置名称和图标,开启所需功能,构建并提交。永久免费更新。
在主页查看方案ChatGPT & Codex
推送是 App 审核最先注意到的原生功能,而大部分工作都在您的 Web 应用和后端里,所以它很适合交给 Codex。WebViewGold 已经在两个原生项目中内置了 OneSignal SDK;Codex 负责设计请求授权的时机,把每台设备的订阅 ID 同步到您的服务器,并编写发送定向通知的接口。下面是分工、AGENTS.md 规则,以及一个能避开常见错误的提示词。
每个平台一次性 $109 起,或由我们的团队全程代劳,$698 起。
目标是面向特定用户的推送:在某一个人的手机上显示“您的评论有新回复”,而不是向所有安装了应用的人群发。这需要三块拼图:在 OneSignal 注册的原生应用、知道哪个 OneSignal 订阅属于哪个用户的服务器,以及一个发送调用,让用户点按通知时在应用内打开正确的页面。
审核人员看到的比您想象的少,但看到得很早。在第一个页面还没渲染出来之前就弹出的授权对话框,看起来就像一个网站毫无理由地索要权限。Apple 的准则 4.5.4 还规定,应用不得以推送作为正常使用的前提;推广类推送需要在您自己的界面中明确征得同意,并提供退订方式。所以:在用户完成一个有意义的操作之后再请求,说明他们会收到什么,并在设置里保留一个开关。这样做的话,推送就是准则 4.2 下最清晰的原生功能信号之一(准则 4.2 对基于 Web 的应用有什么要求)。
本指南假设您使用 React 前端、Express API 和 Firestore,并使用 WebViewGold AGENTS.md 模板中的 User-Agent 辅助函数 isNativeApp()。这套模式适用于任何技术栈。
| 环节 | 位置 | 负责人 |
|---|---|---|
| 配置了 APNs(iOS)和 FCM(Android)凭据的 OneSignal 应用 | OneSignal 控制台 | 您,一次性设置 |
| 在原生应用中开启 OneSignal | iOS:在 Config.swift 中把 kPushEnabled 设为 true 并填写 oneSignalID。Android:在 Config.java 中把 PUSH_ENABLED 设为 true,并在 build.gradle 的 ONE_SIGNAL_APP_ID 中填写 App ID | 您,或在原生项目中使用 Codex |
| 关闭首次启动时的授权弹窗 | askforpushpermissionatfirstrun 设为 false(Config.swift),requirePush 设为 false(Config.java) | 您 |
| 触发原生对话框的授权说明面板 | Web 应用,跳转到 registerpush:// | Codex |
| 把订阅 ID 发送到您的服务器 | getonesignalplayerid:// 会填充 onesignalplayerid;或者开启 autoInjectVariable / AUTO_INJECT_VARIABLES;或者通过 kPushEnhanceUrl / PUSH_ENHANCE_WEBVIEW_URL 在首个 URL 上接收 ?onesignal_push_id= | Codex(Web 和 Express) |
| 设置中的状态显示 | checkNativePushPermissions:// 会填充 nativePushEnabled;settingsapp:// 会打开该应用的系统设置 | Codex |
| 发送 | Express 调用 OneSignal REST API | Codex 编写,您来审阅 |
把这段追加到您的 AGENTS.md 中。Codex 在每次会话开始时都会读取它;当您在 pull request 中评论 @codex review 时,Codex 代码审查会遵循 Review guidelines 标题下的规则。所有 scheme 和配置项都取自 iOS 推送文档和 Android 推送文档。
## 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.
在过去5年我们尝试过的所有应用平台中,WebViewGold为我们带来了有史以来最短、最高效的上市时间。我不知道为什么没有更早发现你们,但我真的非常满意。目前我们的应用在iOS App Store和Google Play商店的下载量已超过6000次,没有任何问题!
在仓库中以工作区写入权限、按需审批的方式启动 Codex:codex --sandbox workspace-write --ask-for-approval on-request。在这种模式下网络默认是关闭的,这正适合本任务:它应该产出代码和模拟测试,而不是真实的通知。请替换方括号中的内容。
请先阅读 AGENTS.md,尤其是 WebViewGold 桥接和推送部分。然后在这个仓库中端到端地实现推送通知。不要改动原生项目。
背景:原生 iOS 和 Android 应用通过 WebViewGold 使用 OneSignal。首次启动时的授权弹窗已关闭(Config.swift 中 askforpushpermissionatfirstrun = false,Config.java 中 requirePush = false),所以由我们的 Web 应用决定何时请求授权。
1. 授权请求:在用户[创建第一个项目]之后,显示一个简短的面板,说明他们会收到哪些通知([回复、提醒]),并提供“开启”和“以后再说”两个按钮。当 isNativeApp() 为 true 时,“开启”跳转到 registerpush://。“以后再说”记住 14 天。绝不在浏览器中显示这个面板。
2. 订阅同步:在登录后和用户同意授权后,跳转到 getonesignalplayerid:// 并等待 window.onesignalplayerid(每 200 毫秒轮询一次,5 秒后放弃,忽略空值)。把它连同平台信息 POST 到 /api/push/devices,并用 Firebase ID token 进行身份验证。服务器保存 pushDevices/{subscriptionId} = { uid, platform, updatedAt },这样一部转手的手机会归到新用户名下。退出登录时,先调用 DELETE /api/push/devices/:id,再执行登出。
3. 设置:添加一行“通知”。跳转到 checkNativePushPermissions://,稍等片刻后读取 window.nativePushEnabled。如果为 false,显示“通知已关闭”和一个跳转到 settingsapp:// 的按钮。
4. 发送:编写 server/push/send.js,导出 sendPushToUser(uid, { title, body, path })。它读取该用户的 pushDevices,使用 include_subscription_ids 调用 OneSignal REST API,并把深度链接(我们的生产域名 + path)放进 data.url。绝不使用顶层的 url 字段。从环境变量读取 ONESIGNAL_APP_ID 和 ONESIGNAL_API_KEY,并把这两个变量(不带值)添加到 .env.example。在[新回复的处理函数]中调用它。
5. 测试:为设备路由编写单元测试(需要身份验证、upsert、只能删除自己的记录),并在模拟 fetch 的情况下测试 sendPushToUser。不要调用真实的 OneSignal API。
6. 最后列出:修改过的文件、我必须设置的环境变量,以及一份编号的真机测试清单。审阅 diff 时以此为标准。最重要的有两点:API 密钥永远不离开服务器,深度链接放在 data.url 中。OneSignal 的顶层 url 是 Launch URL,会打开浏览器;WebViewGold 文档明确要求改用附加数据字段 url。
// 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;
}预留一个下午;大部分时间都在等待构建。
添加 Apple iOS 平台并填入您的 APNs 凭据,再添加 Google Android 平台并上传 OneSignal 要求的 Firebase 服务账号 JSON。复制 App ID 和 App API key。密钥放进服务器的环境变量中,绝不能写进 AGENTS.md、您共享的云端任务环境或客户端代码包。
iOS:kPushEnabled 设为 true,在 oneSignalID 中填写您的 App ID,askforpushpermissionatfirstrun 设为 false,并为您的 Bundle ID 启用 Push Notifications 功能。Android:PUSH_ENABLED 设为 true,在 build.gradle 的 ONE_SIGNAL_APP_ID 中填写 App ID,requirePush 设为 false。Config.swift 和 Config.java 都是纯文本,所以在 WebViewGold 项目中开第二个 Codex 会话就能准确设置这些值;签名仍由您自己负责。
粘贴上面的提示词。接受结果之前,先在 CLI 中输入 /review,让它对未提交的改动再审查一遍,然后对照 AGENTS.md 规则亲自阅读 git diff。优先检查那两条 P1 规则。
在服务器上设置 ONESIGNAL_APP_ID 和 ONESIGNAL_API_KEY,然后部署 Web 应用和 API。这一步原生项目无需任何改动,这正是把逻辑放在 Web 端的好处。
iOS 用 TestFlight,Android 用内部测试轨道。推送需要在真机上运行签名过的构建版本。手边没有 Mac?Cloud Builder 可以在浏览器表单中设置同样的 WebViewGold 选项,并构建两个应用。
第一次从 OneSignal 控制台发送,并带上附加数据字段 url,以此验证原生端。然后触发您自己的事件(一条回复、一个提醒),以此验证服务器、设备映射和深度链接。
settingsapp:// 打开系统设置。nativePushEnabled 才为 true。Notification.requestPermission()。在应用内,Web 推送是错误的渠道;WebViewGold 已经内置了原生 SDK。window.onesignalplayerid。注入的变量是异步到达的,所以要带超时地轮询。multiapicall://getonesignalplayerid,getappversion 合并数据调用(参见 MultiAPICall 文档)。url 字段,导致用户被带到浏览器。registerpush://。什么都不会发生,按钮看起来就像坏了。可以。WebViewGold 支持 OneSignal、Firebase 和 Pushwoosh。使用 Firebase 时,开启 kFirebasePushEnabled(iOS)或 FIREBASE_PUSH_ENABLED(Android),添加 Google 服务文件,并通过 getfirebaseplayerid:// 读取 token,它会填充 firebaseplayerid。AGENTS.md 规则保持不变,只有变量名和发送 API 不同。
无法端到端测试。推送需要在真机上运行签名过的构建版本,并使用真实凭据。Codex 可以在模拟 fetch 的情况下对设备路由和发送函数做单元测试,这能发现大部分 bug;最后一步是您的真机测试。
因为 WebViewGold 桥接交给您 Web 应用的是订阅 ID(通过 getonesignalplayerid:// 或注入的变量),这是您可以依赖的标识符。把订阅 ID 映射到您自己的用户 ID,也能让退出登录、切换账号和账号删除更容易做对。
只有当推送从第一秒起就是应用的核心时才需要,例如告警类应用。对大多数应用来说,在用户完成有意义的操作后再请求,同意的人会更多,看起来也不像一个网站在索要权限。
它有帮助,但没有哪个单一功能能保证通过审核。审核人员评判的是整体体验:整洁的移动端布局、类似原生应用的导航、离线处理,以及推送和分享等原生细节。
结果相同,只是您需要投入的工作量不同。三种方式都附带审核通过保证和 7×24 小时人工支持。
$109 每个平台,一次性付费
WebViewGold 许可证:一个现成的 Xcode 或 Android Studio 项目。粘贴您的 URL,设置名称和图标,开启所需功能,构建并提交。永久免费更新。
在主页查看方案$349 每个许可证,一次性附加购买
无需 Mac、Xcode 或 Android Studio:在浏览器中完成配置,我们在云端构建两个应用,并将其交付到 App Store Connect 和 Play Console。
Cloud Builder 如何运作 试用免费演示$698 每个平台 $798
把您的 URL 和图标发给我们。我们的团队在 72 小时内完成配置、构建并交付您的应用,白标,附源码项目。两个平台共 $1,158。
Full Service 详情 服务提供方:冬季特惠价格有效期至 2026年12月31日。
App Store & Google Play 审核通过保证 – 否则 14 天内退款
想直接交给我们?Full Service:我们在 72 小时内完成配置、构建并交付,每个平台 $698 起。有疑问?通过 7×24 小时在线聊天联系我们。
退款依据我们 FAQ 中列出的条件:应用因非内容原因被 Apple 和/或 Google 拒审,并在购买后 14 天内提出申请。