@mpgd/cli 0.56.0 → 0.58.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.d.ts +30 -0
- package/dist/capacitor-shell-starter.js +243 -11
- package/dist/index.js +170 -11
- package/dist/legal-site-output.d.ts +18 -0
- package/dist/legal-site-output.js +46 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +3935 -1308
- package/dist/native-deploy-run.js +4 -0
- package/dist/native-shell-store-policy.d.ts +123 -0
- package/dist/native-shell-store-policy.js +816 -0
- package/dist/notifications-starter.js +26 -3
- package/dist/release-state.d.ts +2 -0
- package/dist/release-state.js +4 -2
- package/dist/testflight-submission.d.ts +9 -0
- package/dist/testflight-submission.js +69 -0
- package/package.json +16 -16
- package/templates/lite-overlay/agent/acceptance.md +1 -1
- package/templates/lite-overlay/agent/brief.md +1 -1
- package/templates/lite-overlay/src/lite/deleteAccountButton.ts +173 -0
- package/templates/lite-overlay/src/lite/restorePurchasesButton.ts +63 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +8 -0
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +28 -6
- package/templates/notifications-overlay/common/notifications/deployment.json +2 -1
- package/templates/notifications-overlay/common/notifications/topics.json +3 -0
- package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +8 -3
- package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +111 -2
- package/templates/notifications-overlay/common/src/platform/notifications.ts +36 -4
- package/templates/phaser-game/README.md +72 -0
- package/templates/phaser-game/agent/acceptance.md +4 -1
- package/templates/phaser-game/agent/brief.md +14 -4
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +37 -0
- package/templates/phaser-game/gitignore +11 -0
- package/templates/phaser-game/legal/delete-account.html +133 -0
- package/templates/phaser-game/legal/privacy.html +111 -12
- package/templates/phaser-game/legal/support.html +1 -0
- package/templates/phaser-game/legal/terms.html +1 -0
- package/templates/phaser-game/mpgd.game.json +3 -0
- package/templates/phaser-game/src/env.d.ts +6 -0
- package/templates/phaser-game/src/i18n/messages.ts +86 -1
- package/templates/phaser-game/src/main.ts +69 -8
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +39 -10
- package/templates/phaser-game/src/platform/deleteAccount.ts +334 -0
- package/templates/phaser-game/src/platform/gameServices.ts +55 -10
- package/templates/phaser-game/src/platform/guestIdentity.ts +301 -0
- package/templates/phaser-game/src/platform/restorePurchases.ts +205 -0
- package/templates/phaser-game/src/platform/serverSession.ts +8 -0
- package/templates/phaser-game/src/platform/sessionGatedServices.ts +217 -0
- package/templates/phaser-game/src/scenes/LobbyScene.ts +254 -1
- package/templates/phaser-game/src/scenes/PlayScene.ts +16 -6
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createCapacitorNotificationServiceProvider,
|
|
4
4
|
loadCapacitorNotificationSdks,
|
|
5
5
|
openCapacitorNotificationSettings,
|
|
6
|
+
readCapacitorAdvertisingPushRenderer,
|
|
6
7
|
} from '@mpgd/adapter-capacitor/notifications';
|
|
7
8
|
|
|
8
9
|
import { starterNotificationTopics } from './notifications';
|
|
@@ -38,9 +39,8 @@ export async function createStarterCapacitorNotifications(
|
|
|
38
39
|
...sdks,
|
|
39
40
|
// Android channel names the player sees in system settings, in the
|
|
40
41
|
// device language. The provider creates a channel only for the topics
|
|
41
|
-
// mapped to Android; `mpgd-advertising
|
|
42
|
-
//
|
|
43
|
-
// player can turn off.
|
|
42
|
+
// mapped to Android; advertising has `mpgd-advertising`, a channel of
|
|
43
|
+
// its own the player can turn off.
|
|
44
44
|
channels: korean
|
|
45
45
|
? {
|
|
46
46
|
'mpgd-service': { name: '알림' },
|
|
@@ -54,6 +54,11 @@ export async function createStarterCapacitorNotifications(
|
|
|
54
54
|
},
|
|
55
55
|
advertisingOptOutTitle: korean ? '수신거부' : 'Unsubscribe',
|
|
56
56
|
openSettings: openCapacitorNotificationSettings,
|
|
57
|
+
// Android: the shell renders advertising pushes with the opt-out
|
|
58
|
+
// action when its messaging service is the kit's renderer, which
|
|
59
|
+
// `mpgd target init capacitor --providers notifications` declares; the
|
|
60
|
+
// backend sends advertising only to such an installation.
|
|
61
|
+
advertisingRenderer: readCapacitorAdvertisingPushRenderer,
|
|
57
62
|
}),
|
|
58
63
|
];
|
|
59
64
|
}
|
|
@@ -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
|
|
|
@@ -140,6 +141,114 @@ export function createStarterWorkerNotificationBackend(
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
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
|
+
|
|
143
252
|
/**
|
|
144
253
|
* Whether the game-services URL may carry the session's credentials: any
|
|
145
254
|
* absolute http(s) URL outside production builds, and in production the
|
|
@@ -4,9 +4,12 @@ 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';
|
|
12
|
+
import { onStarterAccountDeleted } from './deleteAccount';
|
|
10
13
|
import { resolveStarterServerSession } from './serverSession';
|
|
11
14
|
import { routeNotificationOpens, type NotificationOpenRoutes } from './notificationOpens';
|
|
12
15
|
import {
|
|
@@ -23,9 +26,13 @@ import {
|
|
|
23
26
|
* The starter topics' purposes follow the notifications RFC (open question 1):
|
|
24
27
|
* `daily-ready` and `streak-at-risk` are `requested` reminders the player
|
|
25
28
|
* turns on, `friend-challenge` is `service`, and `events` is the sample
|
|
26
|
-
* `advertising` topic
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* `advertising` topic. On Android an FCM notification cannot carry the
|
|
30
|
+
* opt-out action the Korean rules require, so the shell renders advertising
|
|
31
|
+
* pushes itself (`mpgd target init capacitor --providers notifications`
|
|
32
|
+
* declares the kit's renderer, and `notifications/deployment.json` sets
|
|
33
|
+
* `shellRendersAdvertising`); the backend sends one only to an installation
|
|
34
|
+
* whose shell has the renderer. A topic whose copy offers a reward, a
|
|
35
|
+
* benefit, or a promotion is advertising: declare it so, never `requested`.
|
|
29
36
|
*/
|
|
30
37
|
export const starterNotificationCatalog = notificationTopicsJson as NotificationTopicCatalog;
|
|
31
38
|
|
|
@@ -56,7 +63,11 @@ export type { StarterNotificationBackend } from './notificationBackend';
|
|
|
56
63
|
* reminder, advertising, or night switch, because nothing could record them.
|
|
57
64
|
*
|
|
58
65
|
* Reddit gets no Worker backend: its notifications backend is the Devvit
|
|
59
|
-
* server, which keeps the opt-in, the records, and the sends. The
|
|
66
|
+
* server, which keeps the opt-in, the records, and the sends. The runtime
|
|
67
|
+
* there runs over the backend `buildGateways/reddit.ts` sets
|
|
68
|
+
* (`createDevvitNotificationsBackend`, through
|
|
69
|
+
* `setStarterHostedNotificationBackend`), which needs no game session: the
|
|
70
|
+
* Devvit server answers for the logged-in Reddit user. The Worker
|
|
60
71
|
* serves the Android, iOS, and Apps in Toss providers and binds a device to
|
|
61
72
|
* the session its `Authorization` header names, through its
|
|
62
73
|
* `GAME_SERVICES_NOTIFICATIONS_AUTH` binding to your identity service
|
|
@@ -68,6 +79,18 @@ export type { StarterNotificationBackend } from './notificationBackend';
|
|
|
68
79
|
export function createStarterNotificationBackend(
|
|
69
80
|
platform: TargetConfiguredGateway,
|
|
70
81
|
): StarterNotificationBackend | undefined {
|
|
82
|
+
// Only a hosted backend set for this config target; other targets keep the Worker path.
|
|
83
|
+
const hosted = platform.notificationsV2 === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: resolveStarterHostedNotificationBackend(platform.configTarget);
|
|
86
|
+
if (hosted !== undefined) {
|
|
87
|
+
return createStarterHostedNotificationBackend(platform, {
|
|
88
|
+
backend: hosted,
|
|
89
|
+
topics: starterNotificationTopics(platform.configTarget),
|
|
90
|
+
defaultLocale: starterNotificationCatalog.defaultLocale,
|
|
91
|
+
log: (message, error) => warnOnce(platform, message, error),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
71
94
|
return createStarterWorkerNotificationBackend(platform, {
|
|
72
95
|
baseUrl: import.meta.env.VITE_MPGD_GAME_SERVICES_URL,
|
|
73
96
|
mode: import.meta.env.MODE,
|
|
@@ -111,6 +134,15 @@ function starterNotificationBackend(platform: TargetConfiguredGateway): StarterN
|
|
|
111
134
|
backend = createStarterNotificationBackend(platform);
|
|
112
135
|
if (backend !== undefined) {
|
|
113
136
|
backends.set(platform, backend);
|
|
137
|
+
const { runtime } = backend;
|
|
138
|
+
// After an account deletion the device stops registering for the
|
|
139
|
+
// deleted player (the server already removed its endpoints), and the
|
|
140
|
+
// next session gets a fresh runtime.
|
|
141
|
+
const stop = onStarterAccountDeleted(async () => {
|
|
142
|
+
stop();
|
|
143
|
+
backends.delete(platform);
|
|
144
|
+
await runtime.unregister();
|
|
145
|
+
});
|
|
114
146
|
}
|
|
115
147
|
}
|
|
116
148
|
return backend;
|
|
@@ -507,3 +507,75 @@ credentials: Game Services reports `missing_authoritative_backend` or
|
|
|
507
507
|
backends require both a non-production authority mode and an explicit
|
|
508
508
|
`allowLocalBackend` opt-in, so they cannot manufacture successful production
|
|
509
509
|
purchase, rewarded-ad, or leaderboard ledger results.
|
|
510
|
+
|
|
511
|
+
### Guest identity
|
|
512
|
+
|
|
513
|
+
Store purchases on Android and iOS are bound to the player of a server
|
|
514
|
+
session: Google Play's `obfuscatedAccountId` and StoreKit's `appAccountToken`
|
|
515
|
+
are derived from that player's id (`src/platform/commerceAccountBinding.ts`),
|
|
516
|
+
and the backend checks them before it grants. The native shell's local player
|
|
517
|
+
(`android-local-player` / `ios-local-player`) is the same on every install, so
|
|
518
|
+
it binds nothing: Play refuses with `ACCOUNT_BINDING_REQUIRED` and StoreKit
|
|
519
|
+
throws an error naming the missing session.
|
|
520
|
+
|
|
521
|
+
Point the game at the kit's reference guest identity Worker
|
|
522
|
+
(`apps/game-identity-worker` in mpgd-kit, deployed per
|
|
523
|
+
`docs/CLOUDFLARE_WORKER_DEPLOY.md`):
|
|
524
|
+
|
|
525
|
+
```sh
|
|
526
|
+
VITE_MPGD_IDENTITY_URL=https://identity.example.com
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`src/platform/guestIdentity.ts` then signs the player in during bootstrap: a
|
|
530
|
+
server-issued guest on the first launch, kept with a rotating refresh token in
|
|
531
|
+
the device's Keychain or Keystore, and the same guest on later launches. The
|
|
532
|
+
server player becomes the game services' `playerId` and the purchases'
|
|
533
|
+
binding, and `setStarterServerSession` sends its access token to the
|
|
534
|
+
game-services Worker, which resolves it through the identity Worker's
|
|
535
|
+
bindings. A session the identity service ended (logout, a replayed refresh
|
|
536
|
+
token, or 180 days without play by default) is replaced by a new guest; without a linked
|
|
537
|
+
account that player's purchases stay with the old one.
|
|
538
|
+
|
|
539
|
+
Offline at launch, the game starts anyway and keeps trying in the background
|
|
540
|
+
(after 5 s, 15 s, 1 min, then every 5 min, and at once when the device reports
|
|
541
|
+
it is online). A purchase or a Restore Purchases tap signs in first; if that
|
|
542
|
+
still fails, the purchase is refused before any store sheet opens (it
|
|
543
|
+
answers `failed` with the retryable diagnostic `STARTER_SESSION_REQUIRED`,
|
|
544
|
+
which the game action coordinator treats as a plain failure; retry with a
|
|
545
|
+
new idempotency key, as for any failed purchase) and the restore control says it could not
|
|
546
|
+
reach the game server and stays tappable as a retry. The purchase, restore,
|
|
547
|
+
and notification services (`src/platform/sessionGatedServices.ts`) come up
|
|
548
|
+
for the server player as soon as a sign-in succeeds, and run the startup
|
|
549
|
+
purchase recovery then; they are never bound to the platform's local
|
|
550
|
+
placeholder, so no purchase or journal record changes owner. Rewarded ads
|
|
551
|
+
stay off for such a run: an ad watched before the session exists could not
|
|
552
|
+
be claimed. The scenes' `player` and `identitySession` are the boot values
|
|
553
|
+
for the whole run (the platform's player when the boot sign-in failed) and
|
|
554
|
+
the server player from the next launch on: key game-owned save data by
|
|
555
|
+
something that does not change, such as the platform storage itself, not by
|
|
556
|
+
`player.playerId`. A native shell only reaches `https:` origins, so test on a
|
|
557
|
+
device or emulator through an https tunnel to a local Worker. Account linking (Sign in with Apple or Google) is not
|
|
558
|
+
part of the reference Worker yet.
|
|
559
|
+
|
|
560
|
+
### Restore Purchases
|
|
561
|
+
|
|
562
|
+
The lobby (Phaser) or a corner button (lite) shows **Restore purchases** on
|
|
563
|
+
targets that sell through a store and have a Game Services client. It calls
|
|
564
|
+
`monetizationRecovery.restorePurchases()` (`src/platform/restorePurchases.ts`):
|
|
565
|
+
the store resynchronizes (StoreKit `AppStore.sync()`; Google Play re-reads owned
|
|
566
|
+
items) and every purchase is verified through the backend ledger before anything
|
|
567
|
+
is granted. App Review expects this control while the catalog sells a
|
|
568
|
+
non-consumable such as `REMOVE_ADS`. On the App Store and Google Play a purchase
|
|
569
|
+
is bound to the server player, so without a server session (see Guest identity)
|
|
570
|
+
the button says to connect to the game server instead of restoring; keep it reachable from the game's own
|
|
571
|
+
store or settings screen when you replace the lobby.
|
|
572
|
+
|
|
573
|
+
A reinstall on Android, a new device, a second device, a logout, or (by
|
|
574
|
+
default) 180 days away make a new guest, and the store then returns the
|
|
575
|
+
purchase's non-consumables bound to the guest that bought them. The kit's
|
|
576
|
+
reference Worker grants a verified restore of such a non-consumable to the new
|
|
577
|
+
guest as well (restore sharing; earlier devices keep it, up to five guests per
|
|
578
|
+
purchase by default), and the control then says **Restored to this device**;
|
|
579
|
+
past the limit it says the purchase is already in use on the maximum number of
|
|
580
|
+
devices. Consumables are never shared. See "Reinstalls and new devices" in the
|
|
581
|
+
kit's `docs/CLOUDFLARE_WORKER_DEPLOY.md` before selling non-consumables.
|
|
@@ -67,7 +67,10 @@ the snapshot bounds for persistent controls.
|
|
|
67
67
|
## Server session
|
|
68
68
|
|
|
69
69
|
Verify the game sets its real server-backed session with `setStarterServerSession`
|
|
70
|
-
before
|
|
70
|
+
before the game services are created (the starter's `guestIdentity.ts` does it
|
|
71
|
+
with `VITE_MPGD_IDENTITY_URL`, a guest backend through `createGuestServerSession`),
|
|
72
|
+
that a store purchase on Android and iOS carries the server player's binding
|
|
73
|
+
(not `android-local-player` / `ios-local-player`), and that the game does
|
|
71
74
|
not rely on the development fallback, which only a game created with
|
|
72
75
|
`--providers notifications` reads. `VITE_MPGD_DEV_NOTIFICATION_SESSION` is local-only evidence:
|
|
73
76
|
a `pnpm build` never signs in with it (its `mpgd-dev` header needs the Vite dev
|
|
@@ -51,10 +51,20 @@ and reconcile existing operations on startup and resume without opening SDK UI.
|
|
|
51
51
|
|
|
52
52
|
## Server session
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
session
|
|
54
|
+
With `VITE_MPGD_IDENTITY_URL` set (the kit's reference guest identity Worker,
|
|
55
|
+
`apps/game-identity-worker`), `src/platform/guestIdentity.ts` signs the player
|
|
56
|
+
in as a server-issued guest during bootstrap, before the game services read
|
|
57
|
+
the session: it keeps the refresh token in the device's Keychain or Keystore,
|
|
58
|
+
calls `setStarterServerSession` with `createGuestServerSession(coordinator)`
|
|
59
|
+
(whose `sessionId` is the stable session-family id), and binds StoreKit and
|
|
60
|
+
Google Play purchases to the server player. A failed boot sign-in is retried
|
|
61
|
+
in the background and before each purchase or restore; the purchase,
|
|
62
|
+
restore, and notification services come up when it succeeds
|
|
63
|
+
(`src/platform/sessionGatedServices.ts`). Without it the native shell's
|
|
64
|
+
local player binds no purchase, and StoreKit and Play refuse with the reason.
|
|
65
|
+
A game with its own identity service sets its session with
|
|
66
|
+
`setStarterServerSession` (`src/platform/serverSession.ts`) before the game
|
|
67
|
+
services are created instead. For local notifications work only (a game created with
|
|
58
68
|
`--providers notifications`), `VITE_MPGD_DEV_NOTIFICATION_SESSION` on the Vite
|
|
59
69
|
dev server in the `development` mode signs in with an unauthenticated
|
|
60
70
|
development session (`src/platform/developmentServerSession.ts`)
|
|
@@ -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, {
|
|
@@ -18,3 +18,14 @@ apps/*/.env
|
|
|
18
18
|
.DS_Store
|
|
19
19
|
*.log
|
|
20
20
|
*.ait
|
|
21
|
+
# Signing and store credentials stay out of Git (use mise or CI secrets).
|
|
22
|
+
# google-services.json and GoogleService-Info.plist are committed on
|
|
23
|
+
# purpose: Firebase documents them as non-secret, and release builds run from
|
|
24
|
+
# a pinned Git checkout that needs them.
|
|
25
|
+
*.p12
|
|
26
|
+
*.p8
|
|
27
|
+
*.mobileprovision
|
|
28
|
+
*.provisionprofile
|
|
29
|
+
*.keystore
|
|
30
|
+
*.jks
|
|
31
|
+
*.pepk
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Delete Your __GAME_TITLE__ Account</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
font-family:
|
|
11
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
12
|
+
line-height: 1.6;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
background: Canvas;
|
|
18
|
+
color: CanvasText;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
main {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
max-width: 760px;
|
|
25
|
+
padding: 48px 24px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1,
|
|
29
|
+
h2 {
|
|
30
|
+
line-height: 1.2;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
a {
|
|
34
|
+
color: LinkText;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
nav {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-wrap: wrap;
|
|
40
|
+
gap: 16px;
|
|
41
|
+
margin: 0 0 32px;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<main>
|
|
47
|
+
<nav aria-label="Legal pages">
|
|
48
|
+
<a href="/privacy/">Privacy</a>
|
|
49
|
+
<a href="/support/">Support</a>
|
|
50
|
+
<a href="/terms/">Terms</a>
|
|
51
|
+
<a href="/delete-account/" aria-current="page">Delete account</a>
|
|
52
|
+
</nav>
|
|
53
|
+
<!--
|
|
54
|
+
Google Play: enter this page's URL (https://<your legal site>/delete-account/)
|
|
55
|
+
in Play Console > App content > Data safety > "Delete account URL".
|
|
56
|
+
The page must name the app as the store listing does, let players
|
|
57
|
+
request deletion without reinstalling the game, and say what is
|
|
58
|
+
deleted and what is kept and for how long. Starting point, not legal
|
|
59
|
+
advice: replace every REPLACE marker in square brackets (mpgd legal
|
|
60
|
+
build and check warn while any remain).
|
|
61
|
+
-->
|
|
62
|
+
<h1>Delete Your __GAME_TITLE__ Account</h1>
|
|
63
|
+
<p>
|
|
64
|
+
This page explains how to delete your __GAME_TITLE__ account and the
|
|
65
|
+
data linked to it. __GAME_TITLE__ is published by [REPLACE: developer
|
|
66
|
+
name exactly as shown on the store listing].
|
|
67
|
+
</p>
|
|
68
|
+
<h2>In the Game</h2>
|
|
69
|
+
<ol>
|
|
70
|
+
<li>Open __GAME_TITLE__ and go to [REPLACE: menu path, for example the lobby's Delete account button].</li>
|
|
71
|
+
<li>Tap <strong>Delete account</strong>, read what is deleted, and tap <strong>Delete permanently</strong>.</li>
|
|
72
|
+
<li>
|
|
73
|
+
The game deletes your account and signs you out. Anything our server
|
|
74
|
+
cannot finish at once is completed automatically within
|
|
75
|
+
[REPLACE: period, for example 24 hours].
|
|
76
|
+
</li>
|
|
77
|
+
</ol>
|
|
78
|
+
<h2>Without the Game</h2>
|
|
79
|
+
<p>
|
|
80
|
+
If you no longer have the game installed, you do not need to reinstall
|
|
81
|
+
it. Send a deletion request to
|
|
82
|
+
[REPLACE: support email address or request form URL] with the subject
|
|
83
|
+
"Delete my account" and [REPLACE: what lets you find the account,
|
|
84
|
+
for example the player ID shown in the game's settings, or the store
|
|
85
|
+
order number of a purchase made with it]. We delete the account within
|
|
86
|
+
[REPLACE: period, for example 30 days] and confirm by reply.
|
|
87
|
+
[REPLACE: what happens to a guest account that has neither, for
|
|
88
|
+
example "If you cannot give either, we cannot find your account; its
|
|
89
|
+
data is deleted after N months without play."]
|
|
90
|
+
</p>
|
|
91
|
+
<h2>What Is Deleted</h2>
|
|
92
|
+
<ul>
|
|
93
|
+
<li>Your player account and every sign-in session on every device.</li>
|
|
94
|
+
<li>Scores and leaderboard entries.</li>
|
|
95
|
+
<li>[REPLACE: game progress, if our server stores it; otherwise delete this item.]</li>
|
|
96
|
+
<li>Rewarded-ad records.</li>
|
|
97
|
+
<li>Notification tokens, notification settings, and your consent records for advertising messages.</li>
|
|
98
|
+
</ul>
|
|
99
|
+
<h2>What We Keep, and for How Long</h2>
|
|
100
|
+
<ul>
|
|
101
|
+
<li>
|
|
102
|
+
<strong>Purchase records</strong> (the product, the store's order or
|
|
103
|
+
transaction number, and the date) for 5 years from the purchase or its
|
|
104
|
+
last refund, whichever is later, for payment and supply record
|
|
105
|
+
obligations [REPLACE: confirm with counsel which apply to you, for
|
|
106
|
+
example Korea's Act on Consumer Protection in Electronic Commerce].
|
|
107
|
+
They are kept separately and no longer contain your player ID (the
|
|
108
|
+
store's own order number still identifies the purchase), and they
|
|
109
|
+
also stop the same store purchase from being used twice. They are
|
|
110
|
+
deleted after that period.
|
|
111
|
+
</li>
|
|
112
|
+
<li>[REPLACE: anything else the game keeps, such as support messages, with the period.]</li>
|
|
113
|
+
</ul>
|
|
114
|
+
<h2>Purchases</h2>
|
|
115
|
+
<p>
|
|
116
|
+
Deleting your account does not refund purchases. Items you buy once
|
|
117
|
+
(such as removing ads) stay owned by your Google Play or Apple
|
|
118
|
+
account: if you play again, Restore purchases gives them to your new
|
|
119
|
+
game account as a new record, and the deleted account is never
|
|
120
|
+
restored. Consumable items (such as coins) are not restored. To ask
|
|
121
|
+
for a refund, contact Google Play or Apple. A purchase still pending when
|
|
122
|
+
the account is deleted (waiting for a payment or a parent's approval)
|
|
123
|
+
is never delivered: Google Play refunds it automatically, but an App
|
|
124
|
+
Store purchase is not refunded automatically; ask Apple for the
|
|
125
|
+
refund. [REPLACE: subscriptions, if the
|
|
126
|
+
game sells any: cancel them in the store first, because billing
|
|
127
|
+
continues through the store.]
|
|
128
|
+
</p>
|
|
129
|
+
<h2>Contact</h2>
|
|
130
|
+
<p>[REPLACE: support email or form, and the data protection contact if required.]</p>
|
|
131
|
+
</main>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|