@mpgd/cli 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capacitor-shell-starter.js +130 -3
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +2091 -220
- package/dist/notifications-starter.d.ts +29 -2
- package/dist/notifications-starter.js +63 -4
- package/package.json +18 -18
- package/templates/lite-overlay/agent/acceptance.md +1 -0
- package/templates/lite-overlay/agent/brief.md +1 -0
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +91 -3
- package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +113 -3
- package/templates/notifications-overlay/common/src/platform/notifications.ts +19 -1
- package/templates/phaser-game/agent/acceptance.md +10 -0
- package/templates/phaser-game/agent/brief.md +12 -0
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +37 -0
- package/templates/phaser-game/src/env.d.ts +5 -0
- package/templates/phaser-game/src/platform/developmentServerSession.ts +104 -0
- package/templates/phaser-game/src/platform/serverSession.ts +49 -26
|
@@ -61,9 +61,36 @@ export declare const notificationsCapacitorSupportFiles: readonly string[];
|
|
|
61
61
|
/**
|
|
62
62
|
* Files of the base game template the notifications modules import, which a
|
|
63
63
|
* game created by an earlier CLI release may not have: written from the
|
|
64
|
-
* template when missing, kept when the game has one.
|
|
64
|
+
* template when missing, kept when the game has one. A file listed in
|
|
65
|
+
* `notificationsCapacitorTemplateSupportImporters` is written only together
|
|
66
|
+
* with its importer.
|
|
65
67
|
*/
|
|
66
|
-
export declare const notificationsCapacitorTemplateSupportFiles: readonly
|
|
68
|
+
export declare const notificationsCapacitorTemplateSupportFiles: readonly ["src/platform/serverSession.ts", "src/platform/developmentServerSession.ts"];
|
|
69
|
+
/**
|
|
70
|
+
* Template support files only one other support file imports: the importer,
|
|
71
|
+
* and the warning for a game that keeps its own importer without importing
|
|
72
|
+
* the file (an earlier `serverSession.ts`). Such a game would never import the
|
|
73
|
+
* file, and its `@mpgd/game-services` may predate what the file imports, so
|
|
74
|
+
* the file is not written for it. An importer that imports it gets it back.
|
|
75
|
+
*/
|
|
76
|
+
export declare const notificationsCapacitorTemplateSupportImporters: Readonly<Partial<Record<(typeof notificationsCapacitorTemplateSupportFiles)[number], {
|
|
77
|
+
readonly importer: (typeof notificationsCapacitorTemplateSupportFiles)[number];
|
|
78
|
+
readonly warning: string;
|
|
79
|
+
/**
|
|
80
|
+
* A kit export the file imports, which the game's installed package
|
|
81
|
+
* must have before the file is written (an older package lacks it).
|
|
82
|
+
*/
|
|
83
|
+
readonly requires?: {
|
|
84
|
+
readonly packageName: string;
|
|
85
|
+
readonly exportName: string;
|
|
86
|
+
};
|
|
87
|
+
}>>>;
|
|
88
|
+
/**
|
|
89
|
+
* Whether `source` names the module `./<name>` (with or without `.js` or
|
|
90
|
+
* `.ts`) as a string outside comments: any static, side-effect, or dynamic
|
|
91
|
+
* import or re-export of it. A commented-out import does not count.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sourceReferencesRelativeModule(source: string, name: string): boolean;
|
|
67
94
|
/** Whether a Capacitor gateway already registers a notifications provider. */
|
|
68
95
|
export declare function capacitorGatewayRegistersNotifications(source: string): boolean;
|
|
69
96
|
/**
|
|
@@ -64,6 +64,15 @@ const commonEdits = {
|
|
|
64
64
|
'src/platform/buildGateways/capacitorAndroid.ts': [gatewayImport, gatewayProviders('android')],
|
|
65
65
|
'src/platform/buildGateways/capacitorIos.ts': [gatewayImport, gatewayProviders('ios')],
|
|
66
66
|
'src/platform/buildGateways/reddit.ts': [
|
|
67
|
+
{
|
|
68
|
+
anchor: ' createDevvitCommerceProducts,\n createDevvitPlatformGateway,\n',
|
|
69
|
+
replacement: [
|
|
70
|
+
' createDevvitCommerceProducts,',
|
|
71
|
+
' createDevvitNotificationsBackend,',
|
|
72
|
+
' createDevvitPlatformGateway,',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n'),
|
|
75
|
+
},
|
|
67
76
|
{
|
|
68
77
|
anchor: "import { createDevvitWebPaymentsClient } from '@mpgd/adapter-devvit/web';\n",
|
|
69
78
|
replacement: [
|
|
@@ -77,6 +86,7 @@ const commonEdits = {
|
|
|
77
86
|
{
|
|
78
87
|
anchor: "import type { RuntimeConfig } from '../runtimeDetector';\n",
|
|
79
88
|
replacement: [
|
|
89
|
+
"import { setStarterHostedNotificationBackend } from '../notificationBackend';",
|
|
80
90
|
"import { starterNotificationTopics } from '../notifications';",
|
|
81
91
|
"import type { RuntimeConfig } from '../runtimeDetector';",
|
|
82
92
|
'',
|
|
@@ -102,7 +112,14 @@ const commonEdits = {
|
|
|
102
112
|
' runtime: RuntimeConfig,',
|
|
103
113
|
"): Pick<Parameters<typeof createDevvitPlatformGateway>[0], 'notifications'> {",
|
|
104
114
|
' const topics = starterNotificationTopics(runtime.configTarget).map((topic) => topic.id);',
|
|
105
|
-
'
|
|
115
|
+
' if (topics.length === 0) {',
|
|
116
|
+
' return {};',
|
|
117
|
+
' }',
|
|
118
|
+
" // The runtime's backend is the Devvit server (consent records, reminders, notices for the",
|
|
119
|
+
' // logged-in Reddit user), over the same server bridge as the gateway; created on first use.',
|
|
120
|
+
' setStarterHostedNotificationBackend(runtime.configTarget, () =>',
|
|
121
|
+
' createDevvitNotificationsBackend({ appVersion: runtime.appVersion, buildId: runtime.buildId }));',
|
|
122
|
+
' return { notifications: { topics } };',
|
|
106
123
|
'}',
|
|
107
124
|
'',
|
|
108
125
|
'function createCommerceOptions(): DevvitGatewayCommerceOptions | undefined {',
|
|
@@ -139,6 +156,7 @@ const commonEdits = {
|
|
|
139
156
|
' createDevvitCommerceBridgeProducts,',
|
|
140
157
|
' createDevvitCommerceV2BridgeHandler,',
|
|
141
158
|
' createDevvitNotificationService,',
|
|
159
|
+
' createDevvitNotificationsBackendBridgeHandler,',
|
|
142
160
|
' createDevvitNotificationsV2BridgeHandler,',
|
|
143
161
|
"} from '@mpgd/adapter-devvit/server';",
|
|
144
162
|
'',
|
|
@@ -182,7 +200,7 @@ const commonEdits = {
|
|
|
182
200
|
' */',
|
|
183
201
|
'const notificationTopicCatalog = notificationTopicsJson as NotificationTopicCatalog;',
|
|
184
202
|
'const redditNotificationTopics = readRedditNotificationTopics();',
|
|
185
|
-
"const notificationService:
|
|
203
|
+
"const notificationService: ReturnType<typeof createDevvitNotificationService>['service'] | undefined =",
|
|
186
204
|
' redditNotificationTopics.length === 0 ? undefined : createNotificationService();',
|
|
187
205
|
'// The opt-in is served only with the service that sends; a refused catalog turns both off.',
|
|
188
206
|
'const notificationsBridge = notificationService === undefined',
|
|
@@ -192,6 +210,10 @@ const commonEdits = {
|
|
|
192
210
|
' currentUserId: currentPlayerId,',
|
|
193
211
|
' topics: redditNotificationTopics,',
|
|
194
212
|
' });',
|
|
213
|
+
"// The webview runtime's backend: consent records, reminders, and notices for the request's user.",
|
|
214
|
+
'const notificationsBackend = notificationService === undefined',
|
|
215
|
+
' ? undefined',
|
|
216
|
+
' : createDevvitNotificationsBackendBridgeHandler({ service: notificationService, context: () => context });',
|
|
195
217
|
'const handleBridgeRequest = createDevvitBridgeHandler({',
|
|
196
218
|
' redis,',
|
|
197
219
|
' currentPlayerId,',
|
|
@@ -199,6 +221,7 @@ const commonEdits = {
|
|
|
199
221
|
' storageKeyNamespace: gameName,',
|
|
200
222
|
' ...(commerce === undefined ? {} : { commerce }),',
|
|
201
223
|
' ...(notificationsBridge === undefined ? {} : { notifications: notificationsBridge }),',
|
|
224
|
+
' ...(notificationsBackend === undefined ? {} : { notificationsBackend }),',
|
|
202
225
|
'});',
|
|
203
226
|
'',
|
|
204
227
|
].join('\n'),
|
|
@@ -230,7 +253,7 @@ const commonEdits = {
|
|
|
230
253
|
' * deployment the service refuses turns notifications off, never the game',
|
|
231
254
|
' * server.',
|
|
232
255
|
' */',
|
|
233
|
-
"function createNotificationService():
|
|
256
|
+
"function createNotificationService(): ReturnType<typeof createDevvitNotificationService>['service'] | undefined {",
|
|
234
257
|
' try {',
|
|
235
258
|
' return createDevvitNotificationService({',
|
|
236
259
|
' redis,',
|
|
@@ -479,11 +502,47 @@ export const notificationsCapacitorSupportFiles = Object.freeze([
|
|
|
479
502
|
/**
|
|
480
503
|
* Files of the base game template the notifications modules import, which a
|
|
481
504
|
* game created by an earlier CLI release may not have: written from the
|
|
482
|
-
* template when missing, kept when the game has one.
|
|
505
|
+
* template when missing, kept when the game has one. A file listed in
|
|
506
|
+
* `notificationsCapacitorTemplateSupportImporters` is written only together
|
|
507
|
+
* with its importer.
|
|
483
508
|
*/
|
|
484
509
|
export const notificationsCapacitorTemplateSupportFiles = Object.freeze([
|
|
485
510
|
'src/platform/serverSession.ts',
|
|
511
|
+
'src/platform/developmentServerSession.ts',
|
|
486
512
|
]);
|
|
513
|
+
/**
|
|
514
|
+
* Template support files only one other support file imports: the importer,
|
|
515
|
+
* and the warning for a game that keeps its own importer without importing
|
|
516
|
+
* the file (an earlier `serverSession.ts`). Such a game would never import the
|
|
517
|
+
* file, and its `@mpgd/game-services` may predate what the file imports, so
|
|
518
|
+
* the file is not written for it. An importer that imports it gets it back.
|
|
519
|
+
*/
|
|
520
|
+
export const notificationsCapacitorTemplateSupportImporters = Object.freeze({
|
|
521
|
+
'src/platform/developmentServerSession.ts': {
|
|
522
|
+
importer: 'src/platform/serverSession.ts',
|
|
523
|
+
requires: { packageName: '@mpgd/game-services', exportName: 'createDevelopmentServerSession' },
|
|
524
|
+
warning: 'src/platform/serverSession.ts predates development notification sessions '
|
|
525
|
+
+ '(VITE_MPGD_DEV_NOTIFICATION_SESSION); to try notifications locally, copy serverSession.ts and '
|
|
526
|
+
+ 'developmentServerSession.ts from the starter template.',
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
/**
|
|
530
|
+
* Whether `source` names the module `./<name>` (with or without `.js` or
|
|
531
|
+
* `.ts`) as a string outside comments: any static, side-effect, or dynamic
|
|
532
|
+
* import or re-export of it. A commented-out import does not count.
|
|
533
|
+
*/
|
|
534
|
+
export function sourceReferencesRelativeModule(source, name) {
|
|
535
|
+
const specifiers = new Set([`./${name}`, `./${name}.js`, `./${name}.ts`]);
|
|
536
|
+
let found = false;
|
|
537
|
+
// One pass over strings and comments, as in capacitorGatewayRegistersNotifications.
|
|
538
|
+
source.replace(/'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\.|[^`\\])*`|\/\/[^\n]*|\/\*[\s\S]*?\*\//gu, (token) => {
|
|
539
|
+
if (!token.startsWith('/') && specifiers.has(token.slice(1, -1))) {
|
|
540
|
+
found = true;
|
|
541
|
+
}
|
|
542
|
+
return token;
|
|
543
|
+
});
|
|
544
|
+
return found;
|
|
545
|
+
}
|
|
487
546
|
/** Whether a Capacitor gateway already registers a notifications provider. */
|
|
488
547
|
export function capacitorGatewayRegistersNotifications(source) {
|
|
489
548
|
// One pass over strings and comments: neither registers anything, and a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"parse5": "7.3.0",
|
|
71
71
|
"postcss": "8.5.28",
|
|
72
72
|
"postcss-value-parser": "4.2.0",
|
|
73
|
-
"@mpgd/catalog": "0.10.
|
|
74
|
-
"@mpgd/
|
|
75
|
-
"@mpgd/
|
|
76
|
-
"@mpgd/
|
|
73
|
+
"@mpgd/catalog": "0.10.8",
|
|
74
|
+
"@mpgd/platform": "0.28.0",
|
|
75
|
+
"@mpgd/phaser-assets": "0.9.1",
|
|
76
|
+
"@mpgd/target-config": "0.23.8"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/node": "^24.0.0",
|
|
@@ -81,19 +81,19 @@
|
|
|
81
81
|
"@types/yauzl": "3.4.0",
|
|
82
82
|
"ttsc": "0.32.1",
|
|
83
83
|
"typescript": "7.0.2",
|
|
84
|
-
"@mpgd/adapter-ait": "0.20.
|
|
85
|
-
"@mpgd/adapter-browser": "0.11.
|
|
86
|
-
"@mpgd/adapter-capacitor": "0.12.
|
|
87
|
-
"@mpgd/adapter-devvit": "0.
|
|
88
|
-
"@mpgd/adapter-verse8": "0.7.
|
|
89
|
-
"@mpgd/
|
|
90
|
-
"@mpgd/
|
|
91
|
-
"@mpgd/bridge": "0.
|
|
92
|
-
"@mpgd/capacitor-storekit": "0.3.
|
|
93
|
-
"@mpgd/
|
|
94
|
-
"@mpgd/game-services": "0.
|
|
95
|
-
"@mpgd/
|
|
96
|
-
"@mpgd/
|
|
84
|
+
"@mpgd/adapter-ait": "0.20.5",
|
|
85
|
+
"@mpgd/adapter-browser": "0.11.8",
|
|
86
|
+
"@mpgd/adapter-capacitor": "0.12.5",
|
|
87
|
+
"@mpgd/adapter-devvit": "0.14.0",
|
|
88
|
+
"@mpgd/adapter-verse8": "0.7.9",
|
|
89
|
+
"@mpgd/capacitor-play-billing": "0.3.9",
|
|
90
|
+
"@mpgd/game-runtime": "0.8.2",
|
|
91
|
+
"@mpgd/bridge": "0.16.0",
|
|
92
|
+
"@mpgd/capacitor-storekit": "0.3.9",
|
|
93
|
+
"@mpgd/analytics": "0.5.1",
|
|
94
|
+
"@mpgd/game-services": "0.28.1",
|
|
95
|
+
"@mpgd/lite-shell": "0.1.0",
|
|
96
|
+
"@mpgd/i18n": "0.6.22"
|
|
97
97
|
},
|
|
98
98
|
"main": "./dist/index.js",
|
|
99
99
|
"types": "./dist/index.d.ts",
|
|
@@ -11,6 +11,7 @@ Preserve the generated JSON and Markdown in `artifacts/acceptance` or the target
|
|
|
11
11
|
- Verify renderer teardown removes its own listeners and never updates a replacement view from an old promise.
|
|
12
12
|
- Purchases and rewarded-ad callbacks never grant locally; verify authoritative ledger grants and durable reconciliation before release.
|
|
13
13
|
- Keep unknown native outcomes and late recovery observers; closing one lease must not release another owner's presentation or audio.
|
|
14
|
+
- Verify the game sets its real server session with `setStarterServerSession` and does not rely on the development fallback (read only with `--providers notifications`): `VITE_MPGD_DEV_NOTIFICATION_SESSION` is local-only (its `mpgd-dev` header needs the Vite dev server, so a `pnpm build` never signs in with it), and deployed acceptance uses the identity service's sessions.
|
|
14
15
|
|
|
15
16
|
## Reddit Devvit
|
|
16
17
|
|
|
@@ -11,6 +11,7 @@ Read `AGENTS.md`, the agent manifest, and the `use-mpgd-kit` router before editi
|
|
|
11
11
|
- Keep recovery above renderer lifetimes; reconcile existing operations on startup and resume without opening SDK UI.
|
|
12
12
|
- Rewarded v2 needs an application-owned durable encrypted journal. An in-memory store is not recovery evidence.
|
|
13
13
|
- Reserve safe-area geometry once and preserve feed scrolling and browser touch gestures.
|
|
14
|
+
- Set the server session with `setStarterServerSession` in `src/main.ts` before `bootstrap()` (a guest backend through `createGuestServerSession`). In a game created with `--providers notifications`, `VITE_MPGD_DEV_NOTIFICATION_SESSION` on the Vite dev server in the `development` mode is a local-only development session for notifications (`src/platform/developmentServerSession.ts`); it never reaches a build, commerce, or a deployed Worker.
|
|
14
15
|
|
|
15
16
|
Run `pnpm dev`, `pnpm check`, `pnpm build`, and `pnpm accept`.
|
|
16
17
|
Devvit inline and expanded entries both use the lite renderer. Keep optional engines behind explicit dynamic import boundaries and verify the inline budget.
|
|
@@ -74,10 +74,22 @@ game has both:
|
|
|
74
74
|
identity service answers to the Worker's `authenticateNotificationSession`
|
|
75
75
|
for this session's header, the same across token refreshes (see the kit's
|
|
76
76
|
`docs/CLOUDFLARE_WORKER_DEPLOY.md`, "Sessions"). Do not pass a per-token
|
|
77
|
-
or rotating id
|
|
78
|
-
|
|
77
|
+
or rotating id. With a guest-session backend, pass
|
|
78
|
+
`createGuestServerSession(coordinator)` from `@mpgd/game-services`: it
|
|
79
|
+
answers the coordinator's `sessionFamilyId` (not a view's `sessionId`,
|
|
80
|
+
which may rotate on refresh), and refreshes the access token as needed
|
|
81
|
+
(an expired one before the request, one about to expire in the
|
|
82
|
+
background).
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
```ts
|
|
85
|
+
// src/main.ts, before bootstrap()
|
|
86
|
+
const guest = createGuestSessionCoordinator({ backend, credentials, installationId });
|
|
87
|
+
setStarterServerSession(createGuestServerSession(guest));
|
|
88
|
+
void guest.start();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Without either (on the Vite dev server, a development session counts as a
|
|
92
|
+
session; see "Try it locally"), it returns `undefined` and the settings screen asks only
|
|
81
93
|
for the OS or host permission (and withdraws a Devvit opt-in the game
|
|
82
94
|
rendered). With them, the runtime registers the device for the signed-in
|
|
83
95
|
player, keeps its registration state in the platform storage, and
|
|
@@ -92,6 +104,22 @@ game's notifications backend is its Devvit server, which keeps the opt-in,
|
|
|
92
104
|
the records, and the sends, and the Worker serves no `reddit` target. The
|
|
93
105
|
Worker serves the Android, iOS, and Apps in Toss providers.
|
|
94
106
|
|
|
107
|
+
On Reddit the runtime runs over the Devvit server instead:
|
|
108
|
+
`src/platform/buildGateways/reddit.ts` sets
|
|
109
|
+
`createDevvitNotificationsBackend` as the hosted backend
|
|
110
|
+
(`setStarterHostedNotificationBackend`), and the generated Devvit server
|
|
111
|
+
serves it with `createDevvitNotificationsBackendBridgeHandler`. No session
|
|
112
|
+
is needed: the server answers for the logged-in Reddit user, and a
|
|
113
|
+
logged-out player is asked to log in first. Once Reddit approves the app
|
|
114
|
+
and `integrations.notifications` is `available`, the settings screen shows
|
|
115
|
+
the reminder switches there; advertising and night consent stay off,
|
|
116
|
+
because Devvit delivers `service` and `requested` topics only. A reminder
|
|
117
|
+
the game schedules on Reddit (`runtime.reminders.set`) needs a post or
|
|
118
|
+
comment to open: pass `link` (the post id, `context.postId` from
|
|
119
|
+
`@devvit/web/client`), or give the topic's `reddit` mapping a default
|
|
120
|
+
`link`; the `mpgd-notifications-dispatch` task sends it through Devvit when
|
|
121
|
+
it is due.
|
|
122
|
+
|
|
95
123
|
The settings screen shows each
|
|
96
124
|
switch from the latest record, read every time it opens and after every
|
|
97
125
|
change, so a choice made on another device shows. A tap reads the records
|
|
@@ -102,3 +130,63 @@ Night consent adds to 광고성 정보 수신동의: the screen offers it only w
|
|
|
102
130
|
advertising consent is granted, and turning advertising consent off also
|
|
103
131
|
withdraws a granted night consent in the same call (two records, two result
|
|
104
132
|
notices).
|
|
133
|
+
|
|
134
|
+
## Try it locally
|
|
135
|
+
|
|
136
|
+
You can see the settings switches record consent before you have an
|
|
137
|
+
identity service, against the kit's game-services Worker on your machine:
|
|
138
|
+
|
|
139
|
+
1. Fill in `advertisingSender` in `notifications/deployment.json` (the
|
|
140
|
+
Worker refuses a placeholder). In mpgd-kit, give the Worker this game's
|
|
141
|
+
catalog and deployment and the development sessions flag in
|
|
142
|
+
`apps/game-services-worker/.dev.vars` (local variables, never
|
|
143
|
+
committed), then run it with the memory store, the default in its
|
|
144
|
+
`wrangler.toml`:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
cd <mpgd-kit>
|
|
148
|
+
node - <game>/notifications/topics.json <game>/notifications/deployment.json \
|
|
149
|
+
> apps/game-services-worker/.dev.vars <<'EOF'
|
|
150
|
+
const fs = require('node:fs');
|
|
151
|
+
// One line of JSON in single quotes, with any ' written as \u0027.
|
|
152
|
+
const value = (file) =>
|
|
153
|
+
`'${JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8'))).replaceAll("'", '\\u0027')}'`;
|
|
154
|
+
console.log('MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS=true');
|
|
155
|
+
console.log(`MPGD_NOTIFICATIONS_CATALOG=${value(process.argv[2])}`);
|
|
156
|
+
console.log(`MPGD_NOTIFICATIONS_DEPLOYMENT=${value(process.argv[3])}`);
|
|
157
|
+
EOF
|
|
158
|
+
pnpm --dir apps/game-services-worker dev --host 127.0.0.1 --port 8787
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The Worker logs once that sessions are not authenticated. To send real
|
|
162
|
+
pushes to a phone or an emulator, follow mpgd-kit's live test runbook
|
|
163
|
+
(`docs/NOTIFICATIONS_LIVE_TEST.md`) instead of adding the key here: it
|
|
164
|
+
runs this same Worker, with `pnpm notifications:live env` writing the
|
|
165
|
+
variables and the Firebase service-account key to a file outside the
|
|
166
|
+
checkout that you link as `.dev.vars`.
|
|
167
|
+
2. Start this game's dev server with the Worker URL and a development
|
|
168
|
+
session, `<playerId>` or `<playerId>.<installationId>` (letters, digits,
|
|
169
|
+
`_`, `-`):
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
VITE_MPGD_GAME_SERVICES_URL=http://127.0.0.1:8787 \
|
|
173
|
+
VITE_MPGD_DEV_NOTIFICATION_SESSION=player-1 \
|
|
174
|
+
pnpm dev
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`src/platform/developmentServerSession.ts` then signs the game in as that
|
|
178
|
+
player with `Authorization: Bearer mpgd-dev.<playerId>.<sessionId>.<installationId>`,
|
|
179
|
+
which the Worker accepts only with the flag. Anyone can write that header,
|
|
180
|
+
so it authenticates nothing: the session exists only on the Vite dev server
|
|
181
|
+
(`import.meta.env.DEV`) in the `development` mode, and the Worker refuses
|
|
182
|
+
the flag with its D1 store, ignores it once `GAME_SERVICES_NOTIFICATIONS_AUTH`
|
|
183
|
+
is bound, and takes development sessions only on a local host (`localhost`,
|
|
184
|
+
`127.0.0.1`, `[::1]`, `*.localhost`, or `10.0.2.2`, which the Android
|
|
185
|
+
emulator uses for your machine). That check reads the request's host, so
|
|
186
|
+
it protects a deployed Worker, not your network: keep the local Worker on
|
|
187
|
+
loopback, its default. Calling `setStarterServerSession`, even with `undefined`, turns it
|
|
188
|
+
off; the first call after the game used it warns, since the notifications
|
|
189
|
+
backend keeps the session it first read.
|
|
190
|
+
The development session is for the notifications backend only: the
|
|
191
|
+
commerce client keeps the session the game set with `setStarterServerSession`
|
|
192
|
+
(or none), so its requests never carry a development header.
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
import {
|
|
17
17
|
createNotificationRuntime,
|
|
18
18
|
createNotificationRuntimeStorage,
|
|
19
|
+
type NotificationRuntimeBackend,
|
|
19
20
|
} from '@mpgd/platform/notifications-runtime';
|
|
20
21
|
import type { TargetConfiguredGateway } from '@mpgd/target-config';
|
|
21
22
|
|
|
@@ -56,8 +57,8 @@ export interface StarterWorkerNotificationInput {
|
|
|
56
57
|
* Worker. A Reddit game's backend is its Devvit server: the opt-in, the
|
|
57
58
|
* consent records, and the sends live there, and the Worker serves no
|
|
58
59
|
* `reddit` target. A runtime over the Worker would send the game's session
|
|
59
|
-
* header to a backend that cannot use it, so Reddit
|
|
60
|
-
*
|
|
60
|
+
* header to a backend that cannot use it, so Reddit gets the hosted backend
|
|
61
|
+
* below instead (`createStarterHostedNotificationBackend`).
|
|
61
62
|
*/
|
|
62
63
|
const targetsWithoutWorkerBackend: ReadonlySet<string> = new Set(['reddit']);
|
|
63
64
|
|
|
@@ -90,7 +91,8 @@ export function createStarterWorkerNotificationBackend(
|
|
|
90
91
|
if (session === undefined) {
|
|
91
92
|
input.log(
|
|
92
93
|
'[notifications] VITE_MPGD_GAME_SERVICES_URL is set but no server session is: call setStarterServerSession '
|
|
93
|
-
+ '(src/platform/serverSession.ts) before the game starts
|
|
94
|
+
+ '(src/platform/serverSession.ts) before the game starts, or on the dev server set '
|
|
95
|
+
+ 'VITE_MPGD_DEV_NOTIFICATION_SESSION (docs/NOTIFICATIONS.md). The settings screen shows no switch until then.',
|
|
94
96
|
);
|
|
95
97
|
return undefined;
|
|
96
98
|
}
|
|
@@ -139,6 +141,114 @@ export function createStarterWorkerNotificationBackend(
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A backend the target's own server hosts, set by the target's gateway
|
|
146
|
+
* builder for its config target: on Reddit, `createDevvitNotificationsBackend`
|
|
147
|
+
* from `@mpgd/adapter-devvit` over the Devvit server bridge
|
|
148
|
+
* (`buildGateways/reddit.ts`). The server answers for the request's
|
|
149
|
+
* logged-in Reddit user, so the game sets no session of its own. The
|
|
150
|
+
* backend is created on first use, so a game whose target exposes no
|
|
151
|
+
* notifications provider (Reddit's gated beta before approval) sends
|
|
152
|
+
* nothing.
|
|
153
|
+
*/
|
|
154
|
+
let hostedBackend:
|
|
155
|
+
| {
|
|
156
|
+
readonly configTarget: string;
|
|
157
|
+
readonly create: () => NotificationRuntimeBackend;
|
|
158
|
+
created?: NotificationRuntimeBackend;
|
|
159
|
+
/** The factory threw once: notifications stay off, and the failure is logged once. */
|
|
160
|
+
failed?: true;
|
|
161
|
+
}
|
|
162
|
+
| undefined;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sets the backend `configTarget`'s own server hosts, as a factory called on
|
|
166
|
+
* first use; `undefined` clears it.
|
|
167
|
+
*/
|
|
168
|
+
export function setStarterHostedNotificationBackend(
|
|
169
|
+
configTarget: string,
|
|
170
|
+
create: (() => NotificationRuntimeBackend) | undefined,
|
|
171
|
+
): void {
|
|
172
|
+
hostedBackend = create === undefined ? undefined : { configTarget, create };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The backend `configTarget`'s own server hosts, created on the first call;
|
|
177
|
+
* `undefined` when none was set for this config target, or when its factory
|
|
178
|
+
* threw (logged once; not called again until a new factory is set).
|
|
179
|
+
*/
|
|
180
|
+
export function resolveStarterHostedNotificationBackend(configTarget: string): NotificationRuntimeBackend | undefined {
|
|
181
|
+
if (hostedBackend === undefined || hostedBackend.configTarget !== configTarget) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
if (hostedBackend.failed === true) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
hostedBackend.created ??= hostedBackend.create();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// A backend that cannot be created turns notifications off, never the game.
|
|
191
|
+
hostedBackend.failed = true;
|
|
192
|
+
console.error('[notifications] the hosted notifications backend could not be created.', error);
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return hostedBackend.created;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface StarterHostedNotificationInput {
|
|
199
|
+
readonly backend: NotificationRuntimeBackend | undefined;
|
|
200
|
+
/** This target's topics, each with its mapping. */
|
|
201
|
+
readonly topics: readonly NotificationTopicDeclaration[];
|
|
202
|
+
readonly defaultLocale: string;
|
|
203
|
+
/** Logs a message, once per gateway and message. */
|
|
204
|
+
readonly log: (message: string, error?: unknown) => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The notifications runtime over a backend the target's own server hosts
|
|
209
|
+
* (Reddit: the Devvit server's notifications service), when the target
|
|
210
|
+
* exposes a notifications provider and its gateway builder set the backend.
|
|
211
|
+
* Otherwise `undefined`.
|
|
212
|
+
*
|
|
213
|
+
* The runtime keeps its registration state in the platform storage and
|
|
214
|
+
* starts here. It learns the player from the backend, which reads the
|
|
215
|
+
* session the server answers, so the settings screen shows the reminder
|
|
216
|
+
* switches once a logged-in Reddit user is known; a logged-out viewer is
|
|
217
|
+
* asked to log in first. It runs without a registry entry comparison: the
|
|
218
|
+
* provider and the server's service come from the same adapter and
|
|
219
|
+
* deployment, and the entry lives in server code the webview does not load.
|
|
220
|
+
*/
|
|
221
|
+
export function createStarterHostedNotificationBackend(
|
|
222
|
+
platform: TargetConfiguredGateway,
|
|
223
|
+
input: StarterHostedNotificationInput,
|
|
224
|
+
): StarterNotificationBackend | undefined {
|
|
225
|
+
const provider = platform.notificationsV2;
|
|
226
|
+
const { backend } = input;
|
|
227
|
+
if (provider === undefined || backend === undefined) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const runtime = createNotificationRuntime({
|
|
232
|
+
provider,
|
|
233
|
+
backend,
|
|
234
|
+
catalog: input.topics,
|
|
235
|
+
defaultLocale: input.defaultLocale,
|
|
236
|
+
storage: createNotificationRuntimeStorage({ storage: platform.storage }),
|
|
237
|
+
onError(error) {
|
|
238
|
+
console.error('[notifications]', error);
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
runtime.start().catch((error: unknown) => {
|
|
242
|
+
console.error('[notifications] the runtime could not start.', error);
|
|
243
|
+
});
|
|
244
|
+
return { runtime, listConsent: () => backend.listConsent() };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// A configuration the runtime refuses turns notifications off, never the game.
|
|
247
|
+
input.log('[notifications] the notifications backend could not be created.', error);
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
142
252
|
/**
|
|
143
253
|
* Whether the game-services URL may carry the session's credentials: any
|
|
144
254
|
* absolute http(s) URL outside production builds, and in production the
|
|
@@ -4,7 +4,9 @@ import type { TargetConfiguredGateway } from '@mpgd/target-config';
|
|
|
4
4
|
|
|
5
5
|
import notificationTopicsJson from '../../notifications/topics.json';
|
|
6
6
|
import {
|
|
7
|
+
createStarterHostedNotificationBackend,
|
|
7
8
|
createStarterWorkerNotificationBackend,
|
|
9
|
+
resolveStarterHostedNotificationBackend,
|
|
8
10
|
type StarterNotificationBackend,
|
|
9
11
|
} from './notificationBackend';
|
|
10
12
|
import { resolveStarterServerSession } from './serverSession';
|
|
@@ -56,7 +58,11 @@ export type { StarterNotificationBackend } from './notificationBackend';
|
|
|
56
58
|
* reminder, advertising, or night switch, because nothing could record them.
|
|
57
59
|
*
|
|
58
60
|
* Reddit gets no Worker backend: its notifications backend is the Devvit
|
|
59
|
-
* server, which keeps the opt-in, the records, and the sends. The
|
|
61
|
+
* server, which keeps the opt-in, the records, and the sends. The runtime
|
|
62
|
+
* there runs over the backend `buildGateways/reddit.ts` sets
|
|
63
|
+
* (`createDevvitNotificationsBackend`, through
|
|
64
|
+
* `setStarterHostedNotificationBackend`), which needs no game session: the
|
|
65
|
+
* Devvit server answers for the logged-in Reddit user. The Worker
|
|
60
66
|
* serves the Android, iOS, and Apps in Toss providers and binds a device to
|
|
61
67
|
* the session its `Authorization` header names, through its
|
|
62
68
|
* `GAME_SERVICES_NOTIFICATIONS_AUTH` binding to your identity service
|
|
@@ -68,6 +74,18 @@ export type { StarterNotificationBackend } from './notificationBackend';
|
|
|
68
74
|
export function createStarterNotificationBackend(
|
|
69
75
|
platform: TargetConfiguredGateway,
|
|
70
76
|
): StarterNotificationBackend | undefined {
|
|
77
|
+
// Only a hosted backend set for this config target; other targets keep the Worker path.
|
|
78
|
+
const hosted = platform.notificationsV2 === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: resolveStarterHostedNotificationBackend(platform.configTarget);
|
|
81
|
+
if (hosted !== undefined) {
|
|
82
|
+
return createStarterHostedNotificationBackend(platform, {
|
|
83
|
+
backend: hosted,
|
|
84
|
+
topics: starterNotificationTopics(platform.configTarget),
|
|
85
|
+
defaultLocale: starterNotificationCatalog.defaultLocale,
|
|
86
|
+
log: (message, error) => warnOnce(platform, message, error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
71
89
|
return createStarterWorkerNotificationBackend(platform, {
|
|
72
90
|
baseUrl: import.meta.env.VITE_MPGD_GAME_SERVICES_URL,
|
|
73
91
|
mode: import.meta.env.MODE,
|
|
@@ -64,6 +64,16 @@ Verify safe-area geometry is reserved exactly once: CSS-padded containers must
|
|
|
64
64
|
not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
|
|
65
65
|
the snapshot bounds for persistent controls.
|
|
66
66
|
|
|
67
|
+
## Server session
|
|
68
|
+
|
|
69
|
+
Verify the game sets its real server-backed session with `setStarterServerSession`
|
|
70
|
+
before `bootstrap()` (a guest backend through `createGuestServerSession`) and does
|
|
71
|
+
not rely on the development fallback, which only a game created with
|
|
72
|
+
`--providers notifications` reads. `VITE_MPGD_DEV_NOTIFICATION_SESSION` is local-only evidence:
|
|
73
|
+
a `pnpm build` never signs in with it (its `mpgd-dev` header needs the Vite dev
|
|
74
|
+
server), and acceptance against a deployed Worker uses the identity service's
|
|
75
|
+
sessions.
|
|
76
|
+
|
|
67
77
|
## Shared native presentation
|
|
68
78
|
|
|
69
79
|
- Purchase, rewarded, and interstitial requests use the same game-owned scope.
|
|
@@ -49,6 +49,18 @@ the game-owned action coordinator and authoritative services, with an
|
|
|
49
49
|
application-owned durable encrypted operation store. Keep recovery above scenes
|
|
50
50
|
and reconcile existing operations on startup and resume without opening SDK UI.
|
|
51
51
|
|
|
52
|
+
## Server session
|
|
53
|
+
|
|
54
|
+
Set the game's server-backed session with `setStarterServerSession`
|
|
55
|
+
(`src/platform/serverSession.ts`) before `bootstrap()`; with a guest backend use
|
|
56
|
+
`createGuestServerSession(coordinator)`, whose `sessionId` is the stable
|
|
57
|
+
session-family id. For local notifications work only (a game created with
|
|
58
|
+
`--providers notifications`), `VITE_MPGD_DEV_NOTIFICATION_SESSION` on the Vite
|
|
59
|
+
dev server in the `development` mode signs in with an unauthenticated
|
|
60
|
+
development session (`src/platform/developmentServerSession.ts`)
|
|
61
|
+
against a local Worker run with `MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS`;
|
|
62
|
+
it never reaches a build, the commerce client, or a deployed Worker.
|
|
63
|
+
|
|
52
64
|
## ONE play server-owned monetization
|
|
53
65
|
|
|
54
66
|
Specify issued placement/product IDs, the authenticated reward and checkout intent routes, durable SSV/intent/ledger stores, an encrypted client operation journal, and reconciliation plus atomic cancellation fencing. Keep monetization disabled until these ports are wired. Purchase/ad callbacks are evidence; backend grants precede consume/acknowledge. Separate confirmed checkout closure from financial completion. Plan real-app acceptance with ONEconsole credentials and hosting headers.
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
bridgeStorageLoadProtocol,
|
|
4
4
|
createBridgeError,
|
|
5
5
|
createCommerceV2UnavailableBridgeError,
|
|
6
|
+
createNotificationsBackendUnavailableBridgeError,
|
|
6
7
|
createNotificationsV2UnavailableBridgeError,
|
|
8
|
+
isBridgeNotificationsBackendMethod,
|
|
7
9
|
isBridgeNotificationsV2Method,
|
|
8
10
|
type BridgeRequest,
|
|
9
11
|
type BridgeResponse,
|
|
@@ -68,6 +70,16 @@ export interface DevvitBridgeNotificationsHandler {
|
|
|
68
70
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Serves the nine `notifications.backend.*` bridge methods:
|
|
75
|
+
* `createDevvitNotificationsBackendBridgeHandler` from
|
|
76
|
+
* `@mpgd/adapter-devvit/server` over the Devvit notifications service, for
|
|
77
|
+
* the request's logged-in user.
|
|
78
|
+
*/
|
|
79
|
+
export interface DevvitBridgeNotificationsBackendHandler {
|
|
80
|
+
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
export interface DevvitBridgeHandlerDependencies {
|
|
72
84
|
readonly redis: DevvitBridgeRedisLike;
|
|
73
85
|
/** Authenticated Reddit user ID from the Devvit request context, never from the payload. */
|
|
@@ -91,6 +103,14 @@ export interface DevvitBridgeHandlerDependencies {
|
|
|
91
103
|
* `unavailable`.
|
|
92
104
|
*/
|
|
93
105
|
readonly notifications?: DevvitBridgeNotificationsHandler | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* The notifications runtime's backend: consent records, the player's
|
|
108
|
+
* registration, server reminders, and notices, kept by the Devvit
|
|
109
|
+
* notifications service. Absent, every `notifications.backend.*` method
|
|
110
|
+
* answers `NOTIFICATIONS_CONFIGURATION_REQUIRED`, and the webview's
|
|
111
|
+
* runtime runs without a backend.
|
|
112
|
+
*/
|
|
113
|
+
readonly notificationsBackend?: DevvitBridgeNotificationsBackendHandler | undefined;
|
|
94
114
|
readonly warn?: ((message: string) => void) | undefined;
|
|
95
115
|
}
|
|
96
116
|
|
|
@@ -129,6 +149,23 @@ export function createDevvitBridgeHandler(
|
|
|
129
149
|
);
|
|
130
150
|
}
|
|
131
151
|
}
|
|
152
|
+
// The runtime's backend calls, kept apart from the provider methods above.
|
|
153
|
+
if (isBridgeNotificationsBackendMethod(input.method)) {
|
|
154
|
+
if (dependencies.notificationsBackend === undefined) {
|
|
155
|
+
return createNotificationsBackendUnavailableBridgeError(input, 'Devvit server bridge');
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
return await dependencies.notificationsBackend.request(input);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
warn(`devvit notifications ${input.method} failed: ${errorMessage(error)}`);
|
|
161
|
+
return createBridgeError(
|
|
162
|
+
input.id,
|
|
163
|
+
'NOTIFICATIONS_TEMPORARILY_UNAVAILABLE',
|
|
164
|
+
`${input.method} failed on the Devvit server.`,
|
|
165
|
+
true,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
132
169
|
switch (input.method) {
|
|
133
170
|
case 'runtime.getCapabilities':
|
|
134
171
|
return ok(input, {
|