@mpgd/adapter-capacitor 0.10.1 → 0.11.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.
@@ -0,0 +1,166 @@
1
+ import { type PermissionState, type PluginListenerHandle } from '@capacitor/core';
2
+ /** A notification as `@capacitor-firebase/messaging` reports it. */
3
+ export interface PushSdkNotification {
4
+ readonly id?: string;
5
+ readonly title?: string;
6
+ readonly body?: string;
7
+ /** The FCM data payload (Android) or the APNs `userInfo` (iOS). Untrusted. */
8
+ readonly data?: unknown;
9
+ }
10
+ export interface PushSdkActionEvent {
11
+ /** `tap` for the notification itself, otherwise the action's id. */
12
+ readonly actionId: string;
13
+ readonly notification: PushSdkNotification;
14
+ }
15
+ /** An Android notification channel, as both plugins create it. */
16
+ export interface NotificationSdkChannel {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly description?: string;
20
+ /** Android importance, 1 (min) to 5 (max). */
21
+ readonly importance?: 1 | 2 | 3 | 4 | 5;
22
+ }
23
+ /**
24
+ * Remote notifications through Firebase Cloud Messaging
25
+ * (`@capacitor-firebase/messaging`). iOS registers through Firebase Messaging
26
+ * too, and FCM relays to APNs. Do not install `@capacitor/push-notifications`
27
+ * next to it: on iOS both claim Capacitor's one push handler.
28
+ */
29
+ export interface PushSdk {
30
+ /** On Android 12 and lower the plugin answers `granted` whatever the system setting is. */
31
+ checkPermissions(): Promise<{
32
+ readonly receive: PermissionState;
33
+ }>;
34
+ /** Shows the OS dialog when the OS still allows it; on iOS it also registers with APNs. */
35
+ requestPermissions(): Promise<{
36
+ readonly receive: PermissionState;
37
+ }>;
38
+ /** Rejects while Firebase cannot issue a token yet (iOS before APNs registration, offline). */
39
+ getToken(): Promise<{
40
+ readonly token: string;
41
+ }>;
42
+ createChannel?(channel: NotificationSdkChannel): Promise<void>;
43
+ addListener(eventName: 'tokenReceived', listener: (event: {
44
+ readonly token: string;
45
+ }) => void): Promise<PluginListenerHandle>;
46
+ addListener(eventName: 'notificationReceived', listener: (event: {
47
+ readonly notification: PushSdkNotification;
48
+ }) => void): Promise<PluginListenerHandle>;
49
+ addListener(eventName: 'notificationActionPerformed', listener: (event: PushSdkActionEvent) => void): Promise<PluginListenerHandle>;
50
+ }
51
+ /** A scheduled local notification as the provider writes it. */
52
+ export interface LocalNotificationSdkSchema {
53
+ readonly id: number;
54
+ readonly title: string;
55
+ readonly body: string;
56
+ /** `allowWhileIdle` lets an inexact alarm fire in Doze (`setAndAllowWhileIdle`). */
57
+ readonly schedule: {
58
+ readonly at: Date;
59
+ readonly allowWhileIdle: true;
60
+ };
61
+ /** Round-trips through `getPending` and the listeners; the provider stores its key there. */
62
+ readonly extra: Readonly<Record<string, string>>;
63
+ /** Android only. */
64
+ readonly channelId?: string;
65
+ /**
66
+ * Always `false`: the plugin defaults to exact alarms, which on Android 12+
67
+ * opens the "Alarms & reminders" screen when the app lacks
68
+ * `SCHEDULE_EXACT_ALARM`, and Android 14 denies that permission by default.
69
+ */
70
+ readonly isExactNotification: false;
71
+ }
72
+ /** A local notification as `getPending` and the listeners report it. */
73
+ export interface LocalNotificationSdkEntry {
74
+ readonly id: number;
75
+ /** `getPending` reports these; the provider restores an evicted reminder from them. */
76
+ readonly title?: string;
77
+ readonly body?: string;
78
+ readonly extra?: unknown;
79
+ }
80
+ export interface LocalNotificationSdkActionType {
81
+ readonly id: string;
82
+ readonly actions: {
83
+ readonly id: string;
84
+ readonly title: string;
85
+ /** Opens the app, so the game can route the action. */
86
+ readonly foreground?: boolean;
87
+ }[];
88
+ }
89
+ /**
90
+ * Device-scheduled notifications (`@capacitor/local-notifications`). Its
91
+ * `schedule` asks for the notification permission by itself on Android 13+,
92
+ * so the provider calls it only once a fresh `checkPermissions` answered
93
+ * `granted` (RFC "Consent model": never prompt as a side effect).
94
+ */
95
+ export interface LocalNotificationSdk {
96
+ /** Reflects the system setting on every Android version, unlike the push plugin. */
97
+ checkPermissions(): Promise<{
98
+ readonly display: PermissionState;
99
+ }>;
100
+ requestPermissions(): Promise<{
101
+ readonly display: PermissionState;
102
+ }>;
103
+ schedule(options: {
104
+ readonly notifications: LocalNotificationSdkSchema[];
105
+ }): Promise<unknown>;
106
+ cancel(options: {
107
+ readonly notifications: {
108
+ readonly id: number;
109
+ }[];
110
+ }): Promise<void>;
111
+ getPending(): Promise<{
112
+ readonly notifications: readonly LocalNotificationSdkEntry[];
113
+ }>;
114
+ createChannel(channel: NotificationSdkChannel): Promise<void>;
115
+ /**
116
+ * On iOS this sets the app's notification categories, which remote
117
+ * notifications use too, so it also carries the advertising opt-out
118
+ * category for FCM sends.
119
+ */
120
+ registerActionTypes(options: {
121
+ readonly types: LocalNotificationSdkActionType[];
122
+ }): Promise<void>;
123
+ addListener(eventName: 'localNotificationReceived', listener: (notification: LocalNotificationSdkEntry) => void): Promise<PluginListenerHandle>;
124
+ addListener(eventName: 'localNotificationActionPerformed', listener: (event: {
125
+ readonly actionId: string;
126
+ readonly notification: LocalNotificationSdkEntry;
127
+ }) => void): Promise<PluginListenerHandle>;
128
+ }
129
+ export interface CapacitorNotificationSdks {
130
+ /** Absent when `@capacitor-firebase/messaging` is not installed in the native shell. */
131
+ readonly push?: PushSdk;
132
+ /** Absent when `@capacitor/local-notifications` is not installed in the native shell. */
133
+ readonly local?: LocalNotificationSdk;
134
+ }
135
+ /** What `loadCapacitorNotificationSdks` asks of the Capacitor runtime. */
136
+ export interface CapacitorNotificationSdkRuntime {
137
+ isNativePlatform(): boolean;
138
+ isPluginAvailable(name: string): boolean;
139
+ }
140
+ export interface LoadCapacitorNotificationSdksInput {
141
+ /** Defaults to `Capacitor` from `@capacitor/core`. */
142
+ readonly runtime?: CapacitorNotificationSdkRuntime;
143
+ /** Defaults to `import('@capacitor-firebase/messaging')`. */
144
+ readonly importPush?: () => Promise<{
145
+ readonly FirebaseMessaging: unknown;
146
+ }>;
147
+ /** Defaults to `import('@capacitor/local-notifications')`. */
148
+ readonly importLocal?: () => Promise<{
149
+ readonly LocalNotifications: unknown;
150
+ }>;
151
+ }
152
+ /** The names the two plugins register with Capacitor. */
153
+ export declare const capacitorNotificationPluginNames: Readonly<{
154
+ readonly push: 'FirebaseMessaging';
155
+ readonly local: 'LocalNotifications';
156
+ }>;
157
+ /**
158
+ * Loads the two optional plugins. Each half is absent, so the provider fails
159
+ * closed for it, when the app does not run natively (a browser preview would
160
+ * reach the plugins' web fallbacks), when the shell has no native
161
+ * implementation of the plugin, or when the package cannot be imported. A
162
+ * game that bundles `@mpgd/adapter-capacitor/notifications` installs both
163
+ * peers, so the bundler resolves both imports; the native shell decides which
164
+ * plugin is present.
165
+ */
166
+ export declare function loadCapacitorNotificationSdks(input?: LoadCapacitorNotificationSdksInput): Promise<CapacitorNotificationSdks>;
@@ -0,0 +1,47 @@
1
+ import { Capacitor } from '@capacitor/core';
2
+ /** The names the two plugins register with Capacitor. */
3
+ export const capacitorNotificationPluginNames = Object.freeze({
4
+ push: 'FirebaseMessaging',
5
+ local: 'LocalNotifications',
6
+ });
7
+ /**
8
+ * Loads the two optional plugins. Each half is absent, so the provider fails
9
+ * closed for it, when the app does not run natively (a browser preview would
10
+ * reach the plugins' web fallbacks), when the shell has no native
11
+ * implementation of the plugin, or when the package cannot be imported. A
12
+ * game that bundles `@mpgd/adapter-capacitor/notifications` installs both
13
+ * peers, so the bundler resolves both imports; the native shell decides which
14
+ * plugin is present.
15
+ */
16
+ export async function loadCapacitorNotificationSdks(input = {}) {
17
+ const runtime = input.runtime ?? Capacitor;
18
+ if (!runtime.isNativePlatform()) {
19
+ return {};
20
+ }
21
+ const [push, local] = await Promise.all([
22
+ runtime.isPluginAvailable(capacitorNotificationPluginNames.push)
23
+ ? importPlugin(input.importPush ?? (() => import('@capacitor-firebase/messaging')), 'FirebaseMessaging')
24
+ : undefined,
25
+ runtime.isPluginAvailable(capacitorNotificationPluginNames.local)
26
+ ? importPlugin(input.importLocal ?? (() => import('@capacitor/local-notifications')), 'LocalNotifications')
27
+ : undefined,
28
+ ]);
29
+ return {
30
+ ...(push === undefined ? {} : { push: push.plugin }),
31
+ ...(local === undefined ? {} : { local: local.plugin }),
32
+ };
33
+ }
34
+ /**
35
+ * Imports one plugin, boxed: a Capacitor plugin is a proxy that answers every
36
+ * property, `then` included, so resolving a promise with it would call a
37
+ * native `then` that does not exist. Only the box crosses an `await`.
38
+ */
39
+ async function importPlugin(load, name) {
40
+ try {
41
+ const plugin = (await load())[name];
42
+ return typeof plugin === 'object' && plugin !== null ? { plugin } : undefined;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
@@ -0,0 +1,27 @@
1
+ import { type CreateCapacitorNotificationProviderInput } from './notifications-provider.js';
2
+ import type { CapacitorNotificationServiceProvider } from './providers.js';
3
+ export { capacitorNotificationPluginNames, loadCapacitorNotificationSdks, type CapacitorNotificationSdkRuntime, type CapacitorNotificationSdks, type LoadCapacitorNotificationSdksInput, type LocalNotificationSdk, type LocalNotificationSdkActionType, type LocalNotificationSdkEntry, type LocalNotificationSdkSchema, type NotificationSdkChannel, type PushSdk, type PushSdkActionEvent, type PushSdkNotification, } from './notifications-sdk.js';
4
+ export { capacitorAdvertisingActionTypeId, capacitorIosMaxPendingLocalNotifications, capacitorLocalNotificationId, capacitorNotificationChannelId, capacitorNotificationDataKeys, capacitorNotificationProviderId, capacitorSettingsLeaveTimeoutMs, capacitorSettingsReturnTimeoutMs, createCapacitorNotificationProvider, type CapacitorNotificationAppEvents, type CapacitorNotificationChannelSettings, type CapacitorNotificationProvider, type CreateCapacitorNotificationProviderInput, } from './notifications-provider.js';
5
+ export { createBridgeNotificationProvider, createNotificationProviderBridge, createNotificationServiceProvider, createNotificationSubscriptionAdapter, type CreateBridgeNotificationProviderInput, type CreateNotificationProviderBridgeInput, type CreateNotificationServiceProviderInput, type NotificationProviderBridge, } from './notifications-bridge.js';
6
+ export type { CapacitorNotificationServiceProvider, CapacitorNotificationsV2Service, } from './providers.js';
7
+ export interface CreateCapacitorNotificationServiceProviderInput extends CreateCapacitorNotificationProviderInput {
8
+ /** The registry id; 'capacitor-notifications' when absent. */
9
+ readonly id?: string;
10
+ }
11
+ /**
12
+ * The provider and its registry entry in one call, with the same topics, for
13
+ * `createCapacitorPlatformGateway({ providers: [...] })`:
14
+ *
15
+ * ```ts
16
+ * const sdks = await loadCapacitorNotificationSdks();
17
+ * const notifications = createCapacitorNotificationServiceProvider({
18
+ * platform: 'android',
19
+ * topics: resolveNotificationTopicsForTarget(catalog, 'android'),
20
+ * ...sdks,
21
+ * });
22
+ * ```
23
+ *
24
+ * Create it once, early in startup: the provider registers its native
25
+ * listeners when it is created, which is when a cold-start open reaches it.
26
+ */
27
+ export declare function createCapacitorNotificationServiceProvider(input: CreateCapacitorNotificationServiceProviderInput): CapacitorNotificationServiceProvider;
@@ -0,0 +1,35 @@
1
+ import { createNotificationServiceProvider } from './notifications-bridge.js';
2
+ import { createCapacitorNotificationProvider, } from './notifications-provider.js';
3
+ // `@mpgd/adapter-capacitor/notifications`: the notifications v2 provider for
4
+ // Android and iOS (notifications RFC "Provider profiles", Capacitor). Only
5
+ // this subpath loads the notifications validators and, dynamically, the
6
+ // optional `@capacitor-firebase/messaging` and `@capacitor/local-notifications`
7
+ // peers; the base adapter entry loads neither.
8
+ export { capacitorNotificationPluginNames, loadCapacitorNotificationSdks, } from './notifications-sdk.js';
9
+ export { capacitorAdvertisingActionTypeId, capacitorIosMaxPendingLocalNotifications, capacitorLocalNotificationId, capacitorNotificationChannelId, capacitorNotificationDataKeys, capacitorNotificationProviderId, capacitorSettingsLeaveTimeoutMs, capacitorSettingsReturnTimeoutMs, createCapacitorNotificationProvider, } from './notifications-provider.js';
10
+ export { createBridgeNotificationProvider, createNotificationProviderBridge, createNotificationServiceProvider, createNotificationSubscriptionAdapter, } from './notifications-bridge.js';
11
+ /**
12
+ * The provider and its registry entry in one call, with the same topics, for
13
+ * `createCapacitorPlatformGateway({ providers: [...] })`:
14
+ *
15
+ * ```ts
16
+ * const sdks = await loadCapacitorNotificationSdks();
17
+ * const notifications = createCapacitorNotificationServiceProvider({
18
+ * platform: 'android',
19
+ * topics: resolveNotificationTopicsForTarget(catalog, 'android'),
20
+ * ...sdks,
21
+ * });
22
+ * ```
23
+ *
24
+ * Create it once, early in startup: the provider registers its native
25
+ * listeners when it is created, which is when a cold-start open reaches it.
26
+ */
27
+ export function createCapacitorNotificationServiceProvider(input) {
28
+ const { id, ...providerInput } = input;
29
+ const notifications = createCapacitorNotificationProvider(providerInput);
30
+ return createNotificationServiceProvider({
31
+ id: id ?? 'capacitor-notifications',
32
+ notifications,
33
+ topics: notifications.topics,
34
+ });
35
+ }
@@ -1,7 +1,8 @@
1
- import { type BridgeEvent, type BridgeMethod, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
1
+ import { type BridgeEvent, type BridgeMethod, type BridgeNotificationsEvent, type BridgeNotificationsV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
2
2
  import type { AdProvider } from '@mpgd/platform/ads';
3
- import { type CommerceCapabilities, type PlatformProviderAvailability, type PlatformProviderFeature } from '@mpgd/platform';
3
+ import { type CommerceCapabilities, type NotificationSubscriptionAdapter, type PlatformProviderAvailability, type PlatformProviderFeature } from '@mpgd/platform';
4
4
  import { type CommerceProvider } from '@mpgd/platform/commerce';
5
+ import type { NotificationCapabilities, NotificationProvider } from '@mpgd/platform/notifications';
5
6
  export interface NativeBridge {
6
7
  request(input: BridgeRequest): Promise<BridgeResponse>;
7
8
  /**
@@ -45,8 +46,43 @@ export interface CapacitorServiceProvider {
45
46
  * covers the declared `features`.
46
47
  */
47
48
  readonly commerce?: CommerceProvider;
49
+ /**
50
+ * Optional notifications v2 service (`createNotificationServiceProvider`
51
+ * from `@mpgd/adapter-capacitor/notifications`). The nine
52
+ * `notifications.v2.*` methods are implied, and the gateway serves both
53
+ * `notificationsV2` and the v1 `notifications` adapter from it.
54
+ */
55
+ readonly notificationsV2?: CapacitorNotificationsV2Service;
48
56
  getAvailability(): Promise<Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>>;
49
57
  }
58
+ /**
59
+ * What the gateway needs from a notifications v2 service. It comes from
60
+ * `@mpgd/adapter-capacitor/notifications`, which holds the validators, so the
61
+ * base adapter loads none of them.
62
+ */
63
+ export interface CapacitorNotificationsV2Service {
64
+ /** The service's own capability block, validated and copied. */
65
+ getCapabilities(): Promise<NotificationCapabilities>;
66
+ /** Throws when `data` is not a valid answer to `method`. */
67
+ assertResult(method: BridgeNotificationsV2Method, data: unknown): void;
68
+ /**
69
+ * The game-facing surfaces over the gateway's request path: the
70
+ * `notificationsV2` provider, and the v1 adapter served from it (RFC
71
+ * "Capabilities": an adapter that exposes both backs v1 with v2).
72
+ */
73
+ createGatewaySurface(input: {
74
+ readonly request: <TData>(method: BridgeNotificationsV2Method, payload: unknown) => Promise<TData>;
75
+ readonly subscribe: (listener: (event: BridgeNotificationsEvent) => void) => () => void;
76
+ }): {
77
+ readonly notificationsV2: NotificationProvider;
78
+ readonly notifications: NotificationSubscriptionAdapter;
79
+ };
80
+ }
81
+ /** The provider the registry serves notifications v2 from. */
82
+ export interface CapacitorNotificationServiceProvider extends CapacitorServiceProvider {
83
+ readonly notificationsV2: CapacitorNotificationsV2Service;
84
+ readonly bridge: NativeBridge & Required<Pick<NativeBridge, 'subscribe'>>;
85
+ }
50
86
  /** The provider the registry serves commerce v2 from. */
51
87
  export interface CapacitorCommerceServiceProvider extends CapacitorServiceProvider {
52
88
  readonly commerce: CommerceProvider;
@@ -63,17 +99,34 @@ export interface CapacitorProviderCapabilitySnapshot {
63
99
  * registered.
64
100
  */
65
101
  readonly commerce: CommerceCapabilities;
102
+ /**
103
+ * Present exactly when a notifications v2 service is registered: its
104
+ * validated block, or the degraded block (`availability:
105
+ * 'temporarily-unavailable'`, nothing offered) when that bounded read
106
+ * stalled or failed. `availability.pushNotifications` equals its
107
+ * `availability`.
108
+ */
109
+ readonly notifications?: NotificationCapabilities;
66
110
  }
67
111
  export interface CapacitorProviderRegistry {
68
112
  readonly byMethod: ReadonlyMap<BridgeMethod, CapacitorServiceProvider>;
69
113
  /** The provider that serves commerce v2, when one is registered. */
70
114
  readonly commerce?: CapacitorCommerceServiceProvider;
115
+ /** The provider that serves notifications v2, when one is registered. */
116
+ readonly notifications?: CapacitorNotificationServiceProvider;
71
117
  /** Feature readiness plus the commerce block, from one bounded read per provider. */
72
118
  getCapabilitySnapshot(): Promise<CapacitorProviderCapabilitySnapshot>;
73
119
  assertMethodReady(method: BridgeMethod, payload?: unknown): Promise<void>;
74
120
  getAvailability(): Promise<Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
75
121
  }
76
122
  export declare function createCapacitorProviderRegistry(input: readonly CapacitorServiceProvider[]): CapacitorProviderRegistry;
77
- export declare function assertProviderResponseData(method: BridgeMethod, data: unknown): void;
123
+ export declare function assertProviderResponseData(method: BridgeMethod, data: unknown, provider?: CapacitorServiceProvider): void;
124
+ /**
125
+ * The notifications block a gateway reports while its service cannot be
126
+ * read: nothing offered, `availability: 'temporarily-unavailable'`. The same
127
+ * fields as `createUnsupportedNotificationCapabilities()`, written out so the
128
+ * base adapter loads no notifications module.
129
+ */
130
+ export declare function createDegradedNotificationCapabilities(): NotificationCapabilities;
78
131
  /** The commerce block a gateway reports while its provider cannot be read. */
79
132
  export declare function createDegradedCommerceCapabilities(): CommerceCapabilities;
package/dist/providers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { bridgeCommerceV2Methods, isBridgeCommerceV2Method, isBridgeNotificationsV2Method, } from '@mpgd/bridge';
1
+ import { bridgeCommerceV2Methods, bridgeNotificationsV2Methods, isBridgeCommerceV2Method, isBridgeNotificationsV2Method, } from '@mpgd/bridge';
2
2
  import { isAdMobClientRewardEvidence } from '@mpgd/game-services/admob-client-reward';
3
3
  import { PlatformOperationError, } from '@mpgd/platform';
4
4
  import { createUnsupportedCommerceCapabilities, isCommerceCapabilities, } from '@mpgd/platform/commerce';
@@ -14,6 +14,7 @@ const featureMethods = {
14
14
  const providerFeatures = Object.keys(featureMethods);
15
15
  const providerMethodSet = new Set(Object.values(featureMethods).flat());
16
16
  const impliedCommerceMethods = bridgeCommerceV2Methods;
17
+ const impliedNotificationMethods = bridgeNotificationsV2Methods;
17
18
  const availabilitySet = new Set([
18
19
  'unsupported',
19
20
  'configuration-required',
@@ -33,12 +34,13 @@ export function createCapacitorProviderRegistry(input) {
33
34
  const byFeature = new Map();
34
35
  const ids = new Set();
35
36
  let commerce;
37
+ let notifications;
36
38
  for (const provider of providers) {
37
39
  if (provider.id.trim() === '' || ids.has(provider.id)) {
38
40
  throw new Error(`Capacitor provider ID is empty or registered twice: ${provider.id}`);
39
41
  }
40
42
  ids.add(provider.id);
41
- if (provider.commerce === undefined
43
+ if (provider.commerce === undefined && provider.notificationsV2 === undefined
42
44
  && (provider.features.length === 0 || provider.methods.length === 0)) {
43
45
  throw new Error(`Capacitor provider ${provider.id} must declare features and methods.`);
44
46
  }
@@ -54,16 +56,20 @@ export function createCapacitorProviderRegistry(input) {
54
56
  + 'unavailable as a declared method.');
55
57
  }
56
58
  if (isBridgeNotificationsV2Method(method)) {
57
- // A notifications v2 provider gets its own registry surface (P2); a
58
- // declared method would bypass the pass-through readiness rule.
59
- throw new Error(`Capacitor provider ${provider.id} cannot declare notifications v2 method ${method}; `
60
- + 'notifications v2 is unavailable as a declared method.');
59
+ // A notifications v2 provider is registered through `notificationsV2`;
60
+ // a declared method would bypass the pass-through readiness rule.
61
+ throw new Error(`Capacitor provider ${provider.id} cannot declare notifications v2 method ${method}: `
62
+ + 'notifications v2 is unavailable as a declared method. Register a notifications '
63
+ + 'service through `notificationsV2` instead.');
61
64
  }
62
65
  if (!providerMethodSet.has(method)) {
63
66
  throw new Error(`Capacitor provider ${provider.id} cannot replace base method ${method}.`);
64
67
  }
65
68
  }
66
- const implied = provider.commerce === undefined ? [] : impliedCommerceMethods;
69
+ const implied = [
70
+ ...(provider.commerce === undefined ? [] : impliedCommerceMethods),
71
+ ...(provider.notificationsV2 === undefined ? [] : impliedNotificationMethods),
72
+ ];
67
73
  for (const method of [...methods, ...implied]) {
68
74
  if (byMethod.has(method)) {
69
75
  throw new Error(`Capacitor bridge method ${method} has multiple providers.`);
@@ -76,6 +82,11 @@ export function createCapacitorProviderRegistry(input) {
76
82
  throw new Error(`Capacitor provider ${provider.id} declares a commerce provider but its bridge has no `
77
83
  + 'subscribe; build it with createCommerceServiceProvider.');
78
84
  }
85
+ if (provider.notificationsV2 !== undefined && provider.bridge.subscribe === undefined) {
86
+ // Opens and endpoint changes arrive only as events.
87
+ throw new Error(`Capacitor provider ${provider.id} declares a notifications service but its bridge has `
88
+ + 'no subscribe; build it with createNotificationServiceProvider.');
89
+ }
79
90
  for (const feature of provider.features) {
80
91
  const required = featureMethods[feature];
81
92
  if (required === undefined || byFeature.has(feature)) {
@@ -96,6 +107,16 @@ export function createCapacitorProviderRegistry(input) {
96
107
  // Validated above: the bridge implements `subscribe`.
97
108
  commerce = provider;
98
109
  }
110
+ if (provider.notificationsV2 !== undefined) {
111
+ // Validated above: the bridge implements `subscribe`.
112
+ notifications = provider;
113
+ }
114
+ }
115
+ if (notifications !== undefined && byFeature.has('pushNotifications')) {
116
+ // The v1 adapter is served from the v2 provider, so the two report the
117
+ // same readiness (RFC "Capabilities").
118
+ throw new Error(`Capacitor provider ${byFeature.get('pushNotifications')?.id} declares pushNotifications, `
119
+ + `which the notifications v2 provider ${notifications.id} serves.`);
99
120
  }
100
121
  const readFeatureStates = async (provider) => {
101
122
  const states = {};
@@ -112,13 +133,20 @@ export function createCapacitorProviderRegistry(input) {
112
133
  return states;
113
134
  };
114
135
  const readProviderStates = async (provider) => {
115
- const [states, commerceBlock] = await Promise.all([
136
+ const [states, commerceBlock, notificationsBlock] = await Promise.all([
116
137
  readFeatureStates(provider),
117
138
  provider.commerce === undefined
118
139
  ? Promise.resolve(undefined)
119
140
  : readCommerceCapabilities(provider.commerce),
141
+ provider.notificationsV2 === undefined
142
+ ? Promise.resolve(undefined)
143
+ : readNotificationCapabilities(provider.notificationsV2),
120
144
  ]);
121
- return { states, ...(commerceBlock === undefined ? {} : { commerce: commerceBlock }) };
145
+ return {
146
+ states,
147
+ ...(commerceBlock === undefined ? {} : { commerce: commerceBlock }),
148
+ ...(notificationsBlock === undefined ? {} : { notifications: notificationsBlock }),
149
+ };
122
150
  };
123
151
  const getCapabilitySnapshot = async () => {
124
152
  const states = {};
@@ -126,10 +154,12 @@ export function createCapacitorProviderRegistry(input) {
126
154
  states[feature] = 'unsupported';
127
155
  }
128
156
  let commerceBlock;
157
+ let notificationsBlock;
129
158
  const snapshots = await Promise.all(providers.map(readProviderStates));
130
159
  for (const snapshot of snapshots) {
131
160
  Object.assign(states, snapshot.states);
132
161
  commerceBlock = snapshot.commerce ?? commerceBlock;
162
+ notificationsBlock = snapshot.notifications ?? notificationsBlock;
133
163
  }
134
164
  if (commerce === undefined) {
135
165
  commerceBlock = createUnsupportedCommerceCapabilities();
@@ -139,12 +169,19 @@ export function createCapacitorProviderRegistry(input) {
139
169
  // provider degrades it without rejecting the snapshot.
140
170
  commerceBlock = createDegradedCommerceCapabilities();
141
171
  }
142
- return { availability: states, commerce: commerceBlock };
172
+ if (notifications === undefined) {
173
+ return { availability: states, commerce: commerceBlock };
174
+ }
175
+ notificationsBlock ??= createDegradedNotificationCapabilities();
176
+ // The v1 readiness follows the v2 block it is served from.
177
+ states.pushNotifications = notificationsBlock.availability;
178
+ return { availability: states, commerce: commerceBlock, notifications: notificationsBlock };
143
179
  };
144
180
  const getAvailability = async () => (await getCapabilitySnapshot()).availability;
145
181
  return {
146
182
  byMethod,
147
183
  ...(commerce === undefined ? {} : { commerce }),
184
+ ...(notifications === undefined ? {} : { notifications }),
148
185
  getAvailability,
149
186
  getCapabilitySnapshot,
150
187
  async assertMethodReady(method, payload) {
@@ -212,11 +249,15 @@ export function createCapacitorProviderRegistry(input) {
212
249
  },
213
250
  };
214
251
  }
215
- export function assertProviderResponseData(method, data) {
216
- const valid = isValidProviderData(method, data);
252
+ export function assertProviderResponseData(method, data, provider) {
253
+ const valid = isValidProviderData(method, data, provider);
217
254
  if (!valid) {
218
255
  throw new PlatformOperationError({
219
- code: 'NATIVE_PROVIDER_INVALID_RESPONSE',
256
+ // A notifications v2 answer that breaks the contract is the provider's
257
+ // configuration error (RFC "Bridge and versioning"), as on Apps in Toss.
258
+ code: isBridgeNotificationsV2Method(method)
259
+ ? 'NOTIFICATIONS_CONFIGURATION_REQUIRED'
260
+ : 'NATIVE_PROVIDER_INVALID_RESPONSE',
220
261
  message: `Capacitor provider returned invalid data for ${method}.`,
221
262
  });
222
263
  }
@@ -239,6 +280,36 @@ async function readCommerceCapabilities(provider) {
239
280
  availability: reported.availability,
240
281
  };
241
282
  }
283
+ /**
284
+ * One bounded notifications capability read. The service validates and copies
285
+ * its own block; a stalled or throwing read yields `undefined`.
286
+ */
287
+ async function readNotificationCapabilities(service) {
288
+ return readWithTimeout(Promise.resolve().then(() => service.getCapabilities()));
289
+ }
290
+ /**
291
+ * The notifications block a gateway reports while its service cannot be
292
+ * read: nothing offered, `availability: 'temporarily-unavailable'`. The same
293
+ * fields as `createUnsupportedNotificationCapabilities()`, written out so the
294
+ * base adapter loads no notifications module.
295
+ */
296
+ export function createDegradedNotificationCapabilities() {
297
+ return {
298
+ availability: 'temporarily-unavailable',
299
+ permission: {
300
+ scope: 'app',
301
+ prompt: 'os',
302
+ readable: true,
303
+ serverReadable: false,
304
+ allowance: 'persistent',
305
+ userGesture: false,
306
+ settings: false,
307
+ },
308
+ remote: { supported: false, purposes: [], addressing: 'none', content: 'free-form' },
309
+ local: { supported: false },
310
+ open: 'none',
311
+ };
312
+ }
242
313
  /** The commerce block a gateway reports while its provider cannot be read. */
243
314
  export function createDegradedCommerceCapabilities() {
244
315
  return { ...createUnsupportedCommerceCapabilities(), availability: 'temporarily-unavailable' };
@@ -271,7 +342,7 @@ function isString(value) {
271
342
  function isVoidData(value) {
272
343
  return value === undefined || value === null || (isRecord(value) && Object.keys(value).length === 0);
273
344
  }
274
- function isValidProviderData(method, value) {
345
+ function isValidProviderData(method, value, provider) {
275
346
  if (isBridgeCommerceV2Method(method)) {
276
347
  try {
277
348
  assertCommerceBridgeResult(method, value);
@@ -282,10 +353,19 @@ function isValidProviderData(method, value) {
282
353
  }
283
354
  }
284
355
  if (isBridgeNotificationsV2Method(method)) {
285
- // P2 must validate these answers here, with `assertBridgeNotificationsV2Result`
286
- // from `@mpgd/bridge/notifications`, when it lets a provider serve the
287
- // methods. Until then every notifications v2 answer is rejected.
288
- return false;
356
+ // The service validates with `assertBridgeNotificationsV2Result` from
357
+ // `@mpgd/bridge/notifications`; a provider without one serves no v2
358
+ // method, so its answer is rejected.
359
+ if (provider?.notificationsV2 === undefined) {
360
+ return false;
361
+ }
362
+ try {
363
+ provider.notificationsV2.assertResult(method, value);
364
+ return true;
365
+ }
366
+ catch {
367
+ return false;
368
+ }
289
369
  }
290
370
  switch (method) {
291
371
  case 'ads.preload':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/adapter-capacitor",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Capacitor platform adapter for mpgd Android and iOS shells.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,10 @@
32
32
  "./admob": {
33
33
  "types": "./dist/admob.d.ts",
34
34
  "default": "./dist/admob.js"
35
+ },
36
+ "./notifications": {
37
+ "types": "./dist/notifications.d.ts",
38
+ "default": "./dist/notifications.js"
35
39
  }
36
40
  },
37
41
  "files": [
@@ -39,28 +43,38 @@
39
43
  ],
40
44
  "dependencies": {
41
45
  "@capacitor/app": "8.1.1",
42
- "@mpgd/bridge": "0.13.1",
43
- "@mpgd/capacitor-game-services": "0.7.1",
44
- "@mpgd/game-services": "0.24.1",
45
- "@mpgd/platform": "0.23.1",
46
- "@mpgd/target-config": "0.23.1"
46
+ "@mpgd/bridge": "0.13.2",
47
+ "@mpgd/game-services": "0.25.0",
48
+ "@mpgd/capacitor-game-services": "0.7.2",
49
+ "@mpgd/platform": "0.24.0",
50
+ "@mpgd/target-config": "0.23.2"
47
51
  },
48
52
  "peerDependencies": {
49
53
  "@capacitor-community/admob": "^8.1.0",
50
- "@capacitor/core": "^8.5.1"
54
+ "@capacitor-firebase/messaging": "^8.5.2",
55
+ "@capacitor/core": "^8.5.1",
56
+ "@capacitor/local-notifications": "^8.3.1"
51
57
  },
52
58
  "peerDependenciesMeta": {
53
59
  "@capacitor-community/admob": {
54
60
  "optional": true
61
+ },
62
+ "@capacitor-firebase/messaging": {
63
+ "optional": true
64
+ },
65
+ "@capacitor/local-notifications": {
66
+ "optional": true
55
67
  }
56
68
  },
57
69
  "devDependencies": {
58
70
  "@capacitor-community/admob": "8.1.0",
71
+ "@capacitor-firebase/messaging": "8.5.2",
59
72
  "@capacitor/core": "8.5.2",
73
+ "@capacitor/local-notifications": "8.3.1",
60
74
  "ttsc": "0.32.1",
61
75
  "typescript": "7.0.2",
62
76
  "vitest": "^4.0.0",
63
- "@mpgd/game-runtime": "0.7.1"
77
+ "@mpgd/game-runtime": "0.7.2"
64
78
  },
65
79
  "main": "./dist/index.js",
66
80
  "types": "./dist/index.d.ts",