@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.
- package/README.md +126 -19
- package/dist/bridge-events.d.ts +7 -0
- package/dist/bridge-events.js +14 -0
- package/dist/commerce-bridge.js +2 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +33 -12
- package/dist/notifications-bridge.d.ts +82 -0
- package/dist/notifications-bridge.js +333 -0
- package/dist/notifications-provider.d.ts +118 -0
- package/dist/notifications-provider.js +1023 -0
- package/dist/notifications-sdk.d.ts +166 -0
- package/dist/notifications-sdk.js +47 -0
- package/dist/notifications.d.ts +27 -0
- package/dist/notifications.js +35 -0
- package/dist/providers.d.ts +56 -3
- package/dist/providers.js +98 -18
- package/package.json +22 -8
|
@@ -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
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
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.
|
|
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.
|
|
43
|
-
"@mpgd/
|
|
44
|
-
"@mpgd/game-services": "0.
|
|
45
|
-
"@mpgd/platform": "0.
|
|
46
|
-
"@mpgd/target-config": "0.23.
|
|
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/
|
|
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.
|
|
77
|
+
"@mpgd/game-runtime": "0.7.2"
|
|
64
78
|
},
|
|
65
79
|
"main": "./dist/index.js",
|
|
66
80
|
"types": "./dist/index.d.ts",
|