@mpgd/adapter-capacitor 0.14.0 → 0.16.1
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 +29 -0
- package/dist/crash-reporting.d.ts +104 -0
- package/dist/crash-reporting.js +286 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -0
- package/dist/reviews.d.ts +8 -0
- package/dist/reviews.js +22 -0
- package/package.json +26 -8
package/README.md
CHANGED
|
@@ -366,6 +366,35 @@ is created, which is how a cold-start open reaches it.
|
|
|
366
366
|
because FCM HTTP v1 cannot send to a Firebase installation id and the
|
|
367
367
|
game-services FCM sender refuses them.
|
|
368
368
|
|
|
369
|
+
## Crash reporting
|
|
370
|
+
|
|
371
|
+
`createCapacitorPlatformGateway` exposes `gateway.crashReporter`
|
|
372
|
+
(`@mpgd/platform/crash-reporting`), backed by Firebase Crashlytics when the
|
|
373
|
+
shell has `@capacitor-firebase/crashlytics` 8.x
|
|
374
|
+
(`mpgd target init capacitor --providers crash-reporting`). The adapter
|
|
375
|
+
reaches the native plugin by its registered name, so the game does not
|
|
376
|
+
depend on the npm package; without the plugin the reporter reports
|
|
377
|
+
`not-installed` and does nothing. With it, uncaught WebView errors and
|
|
378
|
+
unhandled rejections are reported as non-fatal (once each, at most 8 per
|
|
379
|
+
session).
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const gateway = createCapacitorPlatformGateway({
|
|
383
|
+
target: 'android',
|
|
384
|
+
appVersion,
|
|
385
|
+
buildId,
|
|
386
|
+
// Optional: extra non-personal keys, or turn the WebView capture off.
|
|
387
|
+
crashReporting: { context: { content_version: 12 }, captureGlobalErrors: true },
|
|
388
|
+
});
|
|
389
|
+
await gateway.crashReporter?.recordError(error);
|
|
390
|
+
await gateway.crashReporter?.setCollectionEnabled(playerAllowsCrashReports);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`crashReporting: false` leaves `crashReporter` out; `crashReporting: { reporter }`
|
|
394
|
+
uses another `CrashReporter`; `crashReporting: { requireConsent: true }` holds
|
|
395
|
+
JavaScript reports until `setCollectionEnabled(true)`. Setup, symbols, consent and the store privacy
|
|
396
|
+
answers are in the [crash reporting guide](../../docs/CRASH_REPORTING.md).
|
|
397
|
+
|
|
369
398
|
## App Store server-side transaction signatures
|
|
370
399
|
|
|
371
400
|
The server-side verifier lives in `@mpgd/game-services/app-store-server`
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type CrashContextValue, type CrashErrorEventTarget, type CrashReporter, type CrashStackFrame } from '@mpgd/platform/crash-reporting';
|
|
2
|
+
/** The name the native plugin registers with Capacitor. */
|
|
3
|
+
export declare const capacitorCrashlyticsPluginName = "FirebaseCrashlytics";
|
|
4
|
+
/** `CrashReporterStatus.provider` of the Capacitor reporter. */
|
|
5
|
+
export declare const capacitorCrashReportingProvider = "firebase-crashlytics";
|
|
6
|
+
type CrashlyticsKeyType = 'string' | 'boolean' | 'long' | 'double' | 'int' | 'float';
|
|
7
|
+
/**
|
|
8
|
+
* The calls the reporter makes on `@capacitor-firebase/crashlytics` 8.x
|
|
9
|
+
* (`FirebaseCrashlyticsPlugin`); the adapter's tests check that the plugin's
|
|
10
|
+
* own declarations satisfy it.
|
|
11
|
+
*/
|
|
12
|
+
export interface CapacitorCrashlyticsPlugin {
|
|
13
|
+
setCustomKey(options: {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly value: CrashContextValue;
|
|
16
|
+
readonly type: CrashlyticsKeyType;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
log(options: {
|
|
19
|
+
readonly message: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
setEnabled(options: {
|
|
22
|
+
readonly enabled: boolean;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
/** iOS only; Android rejects it. */
|
|
25
|
+
isEnabled(): Promise<{
|
|
26
|
+
readonly enabled: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
deleteUnsentReports(): Promise<void>;
|
|
29
|
+
recordException(options: {
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly stacktrace?: CrashStackFrame[];
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface CapacitorCrashReporterOptions {
|
|
35
|
+
readonly target: 'android' | 'ios';
|
|
36
|
+
readonly appVersion: string;
|
|
37
|
+
readonly buildId: string;
|
|
38
|
+
/**
|
|
39
|
+
* Extra non-personal context, for example the kit or content version.
|
|
40
|
+
* Filtered by `sanitizeCrashContext`; never put a player id here.
|
|
41
|
+
*/
|
|
42
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
43
|
+
/**
|
|
44
|
+
* Hold JavaScript reports and breadcrumbs until the player turns
|
|
45
|
+
* collection on with `setCollectionEnabled(true)` (now or in an earlier
|
|
46
|
+
* launch), for games that turn Crashlytics' automatic collection off until
|
|
47
|
+
* the player agrees (docs/CRASH_REPORTING.md#the-players-choice).
|
|
48
|
+
* Defaults to false.
|
|
49
|
+
*/
|
|
50
|
+
readonly requireConsent?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Keeps the player's last `setCollectionEnabled` choice across launches for
|
|
53
|
+
* the JavaScript side, which Android cannot read back from the SDK.
|
|
54
|
+
* Defaults to the WebView's `localStorage`; reads and writes never throw.
|
|
55
|
+
*/
|
|
56
|
+
readonly collectionPreference?: CrashCollectionPreferenceStore;
|
|
57
|
+
/** Defaults to `Capacitor.isPluginAvailable`. */
|
|
58
|
+
readonly isPluginAvailable?: (name: string) => boolean;
|
|
59
|
+
/** Defaults to the plugin registered under `capacitorCrashlyticsPluginName`. */
|
|
60
|
+
readonly plugin?: CapacitorCrashlyticsPlugin;
|
|
61
|
+
}
|
|
62
|
+
/** Where the reporter keeps the player's collection choice between launches. */
|
|
63
|
+
export interface CrashCollectionPreferenceStore {
|
|
64
|
+
read(): boolean | undefined;
|
|
65
|
+
write(enabled: boolean): void;
|
|
66
|
+
}
|
|
67
|
+
/** The default store: the WebView's `localStorage`, ignored when it is unavailable. */
|
|
68
|
+
export declare function createLocalCrashCollectionPreferenceStore(): CrashCollectionPreferenceStore;
|
|
69
|
+
/**
|
|
70
|
+
* A `CrashReporter` backed by Firebase Crashlytics. Native crashes are
|
|
71
|
+
* captured by the Crashlytics SDK on its own; this reporter adds non-fatal
|
|
72
|
+
* reports for JavaScript errors, breadcrumbs, and the collection toggle.
|
|
73
|
+
* Context keys (`mpgd_target`, `mpgd_build_id`, `mpgd_app_version`,
|
|
74
|
+
* `mpgd_adapter`, and `options.context`) are set once, starting when the
|
|
75
|
+
* reporter is created, so native crashes carry them too. When the first of
|
|
76
|
+
* them fails (Firebase not configured) the reporter is `not-configured` and
|
|
77
|
+
* later calls do nothing; a failing game key is skipped.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createCapacitorCrashReporter(options: CapacitorCrashReporterOptions): CrashReporter;
|
|
80
|
+
export interface CapacitorCrashReportingOptions extends Omit<CapacitorCrashReporterOptions, 'target' | 'appVersion' | 'buildId'> {
|
|
81
|
+
/** A reporter to use instead of the Crashlytics one. */
|
|
82
|
+
readonly reporter?: CrashReporter;
|
|
83
|
+
/**
|
|
84
|
+
* Report uncaught errors and unhandled rejections from the WebView.
|
|
85
|
+
* Defaults to true; only installed when the native plugin is present.
|
|
86
|
+
*/
|
|
87
|
+
readonly captureGlobalErrors?: boolean;
|
|
88
|
+
/** Defaults to `globalThis` when it has `addEventListener`. */
|
|
89
|
+
readonly errorTarget?: CrashErrorEventTarget;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The gateway's crash reporting: the reporter and, when the crash service is
|
|
93
|
+
* present, the WebView error capture. `dispose` removes the listeners.
|
|
94
|
+
*/
|
|
95
|
+
export declare function createCapacitorGatewayCrashReporting(input: {
|
|
96
|
+
readonly target: 'android' | 'ios';
|
|
97
|
+
readonly appVersion: string;
|
|
98
|
+
readonly buildId: string;
|
|
99
|
+
readonly options: CapacitorCrashReportingOptions;
|
|
100
|
+
}): {
|
|
101
|
+
readonly reporter: CrashReporter;
|
|
102
|
+
readonly dispose: () => void;
|
|
103
|
+
};
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Capacitor, registerPlugin } from '@capacitor/core';
|
|
2
|
+
import { createUnavailableCrashReporter, formatCrashMessage, installCrashErrorCapture, normalizeCrashError, redactCrashText, sanitizeCrashContext, } from '@mpgd/platform/crash-reporting';
|
|
3
|
+
// Firebase Crashlytics through the native plugin `@capacitor-firebase/crashlytics`
|
|
4
|
+
// 8.x, installed into the shell by `mpgd target init capacitor --providers
|
|
5
|
+
// crash-reporting`. The game bundle never imports the npm package: the
|
|
6
|
+
// reporter reaches the native plugin by its registered name, so a game whose
|
|
7
|
+
// shell lacks the plugin builds unchanged and gets an unavailable reporter.
|
|
8
|
+
/** The name the native plugin registers with Capacitor. */
|
|
9
|
+
export const capacitorCrashlyticsPluginName = 'FirebaseCrashlytics';
|
|
10
|
+
/** `CrashReporterStatus.provider` of the Capacitor reporter. */
|
|
11
|
+
export const capacitorCrashReportingProvider = 'firebase-crashlytics';
|
|
12
|
+
const collectionPreferenceKey = 'mpgd.crashReporting.collectionEnabled';
|
|
13
|
+
/** The default store: the WebView's `localStorage`, ignored when it is unavailable. */
|
|
14
|
+
export function createLocalCrashCollectionPreferenceStore() {
|
|
15
|
+
const storage = () => {
|
|
16
|
+
try {
|
|
17
|
+
return globalThis.localStorage;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
read() {
|
|
25
|
+
try {
|
|
26
|
+
const value = storage()?.getItem(collectionPreferenceKey);
|
|
27
|
+
return value === 'true' ? true : value === 'false' ? false : undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
write(enabled) {
|
|
34
|
+
try {
|
|
35
|
+
storage()?.setItem(collectionPreferenceKey, String(enabled));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// The SDK keeps the native choice; only the JavaScript gate forgets it.
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const int32Min = -(2 ** 31);
|
|
44
|
+
const int32Max = 2 ** 31 - 1;
|
|
45
|
+
function keyType(value) {
|
|
46
|
+
if (typeof value === 'boolean') {
|
|
47
|
+
return 'boolean';
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'number') {
|
|
50
|
+
// The Android plugin reads `long` and `int` values as a Java Integer, so
|
|
51
|
+
// an integer outside the int32 range goes as a double.
|
|
52
|
+
return Number.isInteger(value) && value >= int32Min && value <= int32Max ? 'int' : 'double';
|
|
53
|
+
}
|
|
54
|
+
return 'string';
|
|
55
|
+
}
|
|
56
|
+
/** A kit-set build fact: control characters removed and cut to 128 characters, not redacted. */
|
|
57
|
+
function kitContextValue(value) {
|
|
58
|
+
return value.replace(/[\u0000-\u001f\u007f]/gu, ' ').slice(0, 128);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A `CrashReporter` backed by Firebase Crashlytics. Native crashes are
|
|
62
|
+
* captured by the Crashlytics SDK on its own; this reporter adds non-fatal
|
|
63
|
+
* reports for JavaScript errors, breadcrumbs, and the collection toggle.
|
|
64
|
+
* Context keys (`mpgd_target`, `mpgd_build_id`, `mpgd_app_version`,
|
|
65
|
+
* `mpgd_adapter`, and `options.context`) are set once, starting when the
|
|
66
|
+
* reporter is created, so native crashes carry them too. When the first of
|
|
67
|
+
* them fails (Firebase not configured) the reporter is `not-configured` and
|
|
68
|
+
* later calls do nothing; a failing game key is skipped.
|
|
69
|
+
*/
|
|
70
|
+
export function createCapacitorCrashReporter(options) {
|
|
71
|
+
const isPluginAvailable = options.isPluginAvailable
|
|
72
|
+
?? ((name) => Capacitor.isPluginAvailable(name));
|
|
73
|
+
if (!isPluginAvailable(capacitorCrashlyticsPluginName)) {
|
|
74
|
+
return createUnavailableCrashReporter({
|
|
75
|
+
provider: capacitorCrashReportingProvider,
|
|
76
|
+
state: 'not-installed',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const plugin = options.plugin
|
|
80
|
+
?? registerPlugin(capacitorCrashlyticsPluginName);
|
|
81
|
+
// Build ids are often long hex strings, which the token rule of
|
|
82
|
+
// `redactCrashText` would hide, so the kit's own keys are not redacted.
|
|
83
|
+
// `mpgd_target` goes first: its result tells whether Firebase is configured.
|
|
84
|
+
const kitContext = [
|
|
85
|
+
['mpgd_build_id', kitContextValue(options.buildId)],
|
|
86
|
+
['mpgd_app_version', kitContextValue(options.appVersion)],
|
|
87
|
+
['mpgd_adapter', 'capacitor'],
|
|
88
|
+
];
|
|
89
|
+
const gameContext = Object.entries(sanitizeCrashContext(options.context ?? {}))
|
|
90
|
+
.filter(([key]) => !key.startsWith('mpgd_'));
|
|
91
|
+
// The player's last choice gates JavaScript reports. The SDK keeps it
|
|
92
|
+
// across launches but Android cannot read it back, so the reporter keeps
|
|
93
|
+
// its own copy; on iOS the SDK's value fills a missing one.
|
|
94
|
+
const preference = options.collectionPreference ?? createLocalCrashCollectionPreferenceStore();
|
|
95
|
+
// A game-supplied store may throw; the choice then lives for the session
|
|
96
|
+
// only, and the native toggle still runs.
|
|
97
|
+
const readPreference = () => {
|
|
98
|
+
try {
|
|
99
|
+
return preference.read();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const storedChoice = readPreference();
|
|
106
|
+
let collectionEnabled = storedChoice
|
|
107
|
+
?? (options.requireConsent === true ? false : undefined);
|
|
108
|
+
// The stored choice is applied to the SDK again at every start: an earlier
|
|
109
|
+
// native setEnabled may have failed, leaving the SDK collecting after an
|
|
110
|
+
// opt-out or idle after an opt-in. Until the SDK takes it, the choice is
|
|
111
|
+
// retried with each report or breadcrumb.
|
|
112
|
+
let nativeChoicePending = storedChoice;
|
|
113
|
+
const applyNativeChoice = async () => {
|
|
114
|
+
const enabled = nativeChoicePending;
|
|
115
|
+
if (enabled === undefined || enabled !== collectionEnabled) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await plugin.setEnabled({ enabled });
|
|
120
|
+
if (!enabled) {
|
|
121
|
+
await plugin.deleteUnsentReports();
|
|
122
|
+
}
|
|
123
|
+
if (nativeChoicePending === enabled) {
|
|
124
|
+
nativeChoicePending = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Retried with the next report and at the next start.
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
let ready;
|
|
132
|
+
const prepare = () => {
|
|
133
|
+
ready ??= (async () => {
|
|
134
|
+
try {
|
|
135
|
+
await plugin.setCustomKey({ key: 'mpgd_target', value: options.target, type: 'string' });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
for (const [key, value] of [...kitContext, ...gameContext]) {
|
|
141
|
+
try {
|
|
142
|
+
await plugin.setCustomKey({ key, value, type: keyType(value) });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// One key the SDK refuses does not stop reporting.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (collectionEnabled === undefined && options.target === 'ios') {
|
|
149
|
+
try {
|
|
150
|
+
const { enabled } = await plugin.isEnabled();
|
|
151
|
+
collectionEnabled ??= enabled;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Unknown stays unknown.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await applyNativeChoice();
|
|
158
|
+
return true;
|
|
159
|
+
})();
|
|
160
|
+
return ready;
|
|
161
|
+
};
|
|
162
|
+
// Read after the wait: the player may turn collection off meanwhile.
|
|
163
|
+
const collecting = async () => {
|
|
164
|
+
if (!await prepare()) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
await applyNativeChoice();
|
|
168
|
+
return collectionEnabled !== false;
|
|
169
|
+
};
|
|
170
|
+
// Native crashes can happen before any JavaScript report, so the context
|
|
171
|
+
// keys are set right away. Keys stay on the device until a report is sent,
|
|
172
|
+
// so this does not bypass `requireConsent`.
|
|
173
|
+
void prepare();
|
|
174
|
+
return {
|
|
175
|
+
async getStatus() {
|
|
176
|
+
if (!await prepare()) {
|
|
177
|
+
return { provider: capacitorCrashReportingProvider, state: 'not-configured' };
|
|
178
|
+
}
|
|
179
|
+
let enabled = collectionEnabled;
|
|
180
|
+
if (enabled === undefined && options.target === 'ios') {
|
|
181
|
+
try {
|
|
182
|
+
enabled = (await plugin.isEnabled()).enabled;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
enabled = undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
provider: capacitorCrashReportingProvider,
|
|
190
|
+
state: 'available',
|
|
191
|
+
...(enabled === undefined ? {} : { collectionEnabled: enabled }),
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
async recordError(error, recordOptions) {
|
|
195
|
+
if (!await collecting()) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const normalized = normalizeCrashError(error);
|
|
199
|
+
try {
|
|
200
|
+
await plugin.recordException({
|
|
201
|
+
message: formatCrashMessage(normalized, recordOptions?.source ?? 'game'),
|
|
202
|
+
// The plugin's frames have no column.
|
|
203
|
+
...(normalized.frames.length === 0
|
|
204
|
+
? {}
|
|
205
|
+
: {
|
|
206
|
+
stacktrace: normalized.frames.map(({ columnNumber: _column, ...frame }) => frame),
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// A report that cannot be recorded is dropped; game code never sees it.
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async log(message) {
|
|
217
|
+
if (!await collecting()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await plugin.log({ message: redactCrashText(message) });
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Breadcrumbs are best effort.
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
async setCollectionEnabled(enabled) {
|
|
228
|
+
// Set before any wait, so a report racing with the toggle sees it.
|
|
229
|
+
collectionEnabled = enabled;
|
|
230
|
+
// This call applies the choice itself; only its failure leaves it
|
|
231
|
+
// pending.
|
|
232
|
+
nativeChoicePending = undefined;
|
|
233
|
+
try {
|
|
234
|
+
preference.write(enabled);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Kept for this session; the SDK still persists the native choice.
|
|
238
|
+
}
|
|
239
|
+
if (!await prepare()) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await plugin.setEnabled({ enabled });
|
|
244
|
+
if (!enabled) {
|
|
245
|
+
await plugin.deleteUnsentReports();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// The session flag still gates JavaScript reports; the native choice
|
|
250
|
+
// stays pending and is retried with the next report and next start.
|
|
251
|
+
if (collectionEnabled === enabled) {
|
|
252
|
+
nativeChoicePending = enabled;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The gateway's crash reporting: the reporter and, when the crash service is
|
|
260
|
+
* present, the WebView error capture. `dispose` removes the listeners.
|
|
261
|
+
*/
|
|
262
|
+
export function createCapacitorGatewayCrashReporting(input) {
|
|
263
|
+
const { reporter: customReporter, captureGlobalErrors, errorTarget, ...reporterOptions } = input.options;
|
|
264
|
+
const isPluginAvailable = reporterOptions.isPluginAvailable
|
|
265
|
+
?? ((name) => Capacitor.isPluginAvailable(name));
|
|
266
|
+
const reporter = customReporter ?? createCapacitorCrashReporter({
|
|
267
|
+
...reporterOptions,
|
|
268
|
+
isPluginAvailable,
|
|
269
|
+
target: input.target,
|
|
270
|
+
appVersion: input.appVersion,
|
|
271
|
+
buildId: input.buildId,
|
|
272
|
+
});
|
|
273
|
+
const present = customReporter !== undefined || isPluginAvailable(capacitorCrashlyticsPluginName);
|
|
274
|
+
const target = errorTarget ?? globalErrorTarget();
|
|
275
|
+
const dispose = present && captureGlobalErrors !== false && target !== undefined
|
|
276
|
+
? installCrashErrorCapture({ reporter, target })
|
|
277
|
+
: () => { };
|
|
278
|
+
return { reporter, dispose };
|
|
279
|
+
}
|
|
280
|
+
function globalErrorTarget() {
|
|
281
|
+
const candidate = globalThis;
|
|
282
|
+
return typeof candidate.addEventListener === 'function'
|
|
283
|
+
&& typeof candidate.removeEventListener === 'function'
|
|
284
|
+
? candidate
|
|
285
|
+
: undefined;
|
|
286
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type PlatformGateway, type PlatformTarget, type ViewportAdapter } from '@mpgd/platform';
|
|
2
2
|
import { type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource } from './app-events.js';
|
|
3
|
+
import { type CapacitorCrashReportingOptions } from './crash-reporting.js';
|
|
3
4
|
import { type CapacitorServiceProvider, type NativeBridge } from './providers.js';
|
|
5
|
+
export { capacitorCrashlyticsPluginName, capacitorCrashReportingProvider, createCapacitorCrashReporter, createLocalCrashCollectionPreferenceStore, type CrashCollectionPreferenceStore, type CapacitorCrashlyticsPlugin, type CapacitorCrashReporterOptions, type CapacitorCrashReportingOptions, } from './crash-reporting.js';
|
|
4
6
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, type CapacitorCommerceServiceProvider, type CapacitorNotificationServiceProvider, type CapacitorNotificationsV2Service, type CapacitorProviderCapabilitySnapshot, type CapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
5
7
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, type CommerceProviderBridge, type CreateBridgeCommerceProviderInput, type CreateCommerceProviderBridgeInput, type CreateCommerceServiceProviderInput, } from './commerce-bridge.js';
|
|
6
8
|
export { createStoreCommerceProvider, recoverableErrorCodes, type CreateStoreCommerceProviderInput, type StoreCommerceCapabilityOverrides, type StoreCommerceProductMapping, type StoreCommerceProvider, } from './store-commerce-provider.js';
|
|
@@ -26,4 +28,12 @@ export declare function createCapacitorPlatformGateway(input: {
|
|
|
26
28
|
readonly historyBack?: () => void;
|
|
27
29
|
readonly onAppEventError?: (error: unknown) => void;
|
|
28
30
|
readonly viewport?: ViewportAdapter;
|
|
31
|
+
/**
|
|
32
|
+
* Crash reporting (`gateway.crashReporter`). By default the gateway uses
|
|
33
|
+
* Firebase Crashlytics when the shell has the native plugin (`--providers
|
|
34
|
+
* crash-reporting`) and then reports uncaught WebView errors too; without
|
|
35
|
+
* the plugin the reporter is unavailable and nothing is installed. `false`
|
|
36
|
+
* leaves `crashReporter` out.
|
|
37
|
+
*/
|
|
38
|
+
readonly crashReporting?: CapacitorCrashReportingOptions | false;
|
|
29
39
|
}): PlatformGateway;
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,11 @@ import { withShareIntentCheck } from '@mpgd/platform/routes';
|
|
|
5
5
|
import { createCapacitorAppEvents, } from './app-events.js';
|
|
6
6
|
import { namesOtherProtocol } from './bridge-events.js';
|
|
7
7
|
import { createBridgeCommerceProvider } from './commerce-bridge.js';
|
|
8
|
+
import { createCapacitorGatewayCrashReporting, } from './crash-reporting.js';
|
|
8
9
|
import { assertProviderResponseData, createCapacitorProviderRegistry, } from './providers.js';
|
|
9
10
|
import { assertNotificationsV2Served, externalActivityMethods } from './request-policy.js';
|
|
10
11
|
import { createCapacitorViewport } from './viewport.js';
|
|
12
|
+
export { capacitorCrashlyticsPluginName, capacitorCrashReportingProvider, createCapacitorCrashReporter, createLocalCrashCollectionPreferenceStore, } from './crash-reporting.js';
|
|
11
13
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, } from './providers.js';
|
|
12
14
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, } from './commerce-bridge.js';
|
|
13
15
|
export { createStoreCommerceProvider, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
@@ -99,10 +101,19 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
99
101
|
const viewport = input.viewport ?? createCapacitorViewport({
|
|
100
102
|
...(input.onAppEventError === undefined ? {} : { onError: input.onAppEventError }),
|
|
101
103
|
});
|
|
104
|
+
const crashReporting = input.crashReporting === false
|
|
105
|
+
? undefined
|
|
106
|
+
: createCapacitorGatewayCrashReporting({
|
|
107
|
+
target: input.target,
|
|
108
|
+
appVersion: input.appVersion,
|
|
109
|
+
buildId: input.buildId,
|
|
110
|
+
options: input.crashReporting ?? {},
|
|
111
|
+
});
|
|
102
112
|
const launchedPurchaseHolds = new Set();
|
|
103
113
|
const gatewayLifecycle = {
|
|
104
114
|
...lifecycle,
|
|
105
115
|
async dispose() {
|
|
116
|
+
crashReporting?.dispose();
|
|
106
117
|
try {
|
|
107
118
|
for (const finish of [...launchedPurchaseHolds]) {
|
|
108
119
|
finish();
|
|
@@ -284,6 +295,7 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
284
295
|
};
|
|
285
296
|
},
|
|
286
297
|
...(commerce === undefined ? {} : { commerce }),
|
|
298
|
+
...(crashReporting === undefined ? {} : { crashReporter: crashReporting.reporter }),
|
|
287
299
|
identity: {
|
|
288
300
|
getPlayer: () => request('identity.getPlayer', {}),
|
|
289
301
|
getSession: () => request('identity.getSession', {}),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Capacitor } from '@capacitor/core';
|
|
2
|
+
import { type ReviewProvider } from '@mpgd/platform/reviews';
|
|
3
|
+
export interface CapacitorReviewInput {
|
|
4
|
+
/** Mandatory distribution identity. Android alone must never imply Google Play. */
|
|
5
|
+
readonly store: 'google-play' | 'apple-app-store';
|
|
6
|
+
readonly appId?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createCapacitorReviewProvider(input: CapacitorReviewInput, host?: Pick<typeof Capacitor, 'isNativePlatform' | 'isPluginAvailable' | 'getPlatform'>, plugin?: import("@capawesome/capacitor-app-review").AppReviewPlugin): ReviewProvider;
|
package/dist/reviews.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Capacitor } from '@capacitor/core';
|
|
2
|
+
import { AppReview } from '@capawesome/capacitor-app-review';
|
|
3
|
+
import { createReviewProvider } from '@mpgd/platform/reviews';
|
|
4
|
+
export function createCapacitorReviewProvider(input, host = Capacitor, plugin = AppReview) {
|
|
5
|
+
if (input.store !== 'google-play' && input.store !== 'apple-app-store') {
|
|
6
|
+
throw new TypeError('Unsupported review store');
|
|
7
|
+
}
|
|
8
|
+
const matches = () => host.isNativePlatform() && host.isPluginAvailable('AppReview')
|
|
9
|
+
&& host.getPlatform() === (input.store === 'google-play' ? 'android' : 'ios');
|
|
10
|
+
return createReviewProvider({
|
|
11
|
+
providerId: input.store,
|
|
12
|
+
isAvailable: matches,
|
|
13
|
+
requestNative: () => plugin.requestReview(),
|
|
14
|
+
// iOS listing navigation requires its registered Apple ID; never guess from bundleId.
|
|
15
|
+
...(input.store === 'google-play' || input.appId && /^\d+$/.test(input.appId) ? {
|
|
16
|
+
openPage: async () => {
|
|
17
|
+
await plugin.openAppStore(input.store === 'apple-app-store' && input.appId ? { appId: input.appId } : undefined);
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
} : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Capacitor platform adapter for mpgd Android and iOS shells.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,9 +33,17 @@
|
|
|
33
33
|
"types": "./dist/admob.d.ts",
|
|
34
34
|
"default": "./dist/admob.js"
|
|
35
35
|
},
|
|
36
|
+
"./crash-reporting": {
|
|
37
|
+
"types": "./dist/crash-reporting.d.ts",
|
|
38
|
+
"default": "./dist/crash-reporting.js"
|
|
39
|
+
},
|
|
36
40
|
"./notifications": {
|
|
37
41
|
"types": "./dist/notifications.d.ts",
|
|
38
42
|
"default": "./dist/notifications.js"
|
|
43
|
+
},
|
|
44
|
+
"./reviews": {
|
|
45
|
+
"types": "./dist/reviews.d.ts",
|
|
46
|
+
"default": "./dist/reviews.js"
|
|
39
47
|
}
|
|
40
48
|
},
|
|
41
49
|
"files": [
|
|
@@ -43,38 +51,48 @@
|
|
|
43
51
|
],
|
|
44
52
|
"dependencies": {
|
|
45
53
|
"@capacitor/app": "8.1.1",
|
|
46
|
-
"@mpgd/bridge": "0.16.
|
|
47
|
-
"@mpgd/capacitor-game-services": "0.9.
|
|
48
|
-
"@mpgd/game-services": "0.30.
|
|
49
|
-
"@mpgd/
|
|
50
|
-
"@mpgd/
|
|
54
|
+
"@mpgd/bridge": "0.16.4",
|
|
55
|
+
"@mpgd/capacitor-game-services": "0.9.3",
|
|
56
|
+
"@mpgd/game-services": "0.30.3",
|
|
57
|
+
"@mpgd/platform": "0.32.0",
|
|
58
|
+
"@mpgd/target-config": "0.24.1"
|
|
51
59
|
},
|
|
52
60
|
"peerDependencies": {
|
|
53
61
|
"@capacitor-community/admob": "^8.1.0",
|
|
62
|
+
"@capacitor-firebase/crashlytics": "^8.5.2",
|
|
54
63
|
"@capacitor-firebase/messaging": "^8.5.2",
|
|
55
64
|
"@capacitor/core": "^8.5.1",
|
|
56
|
-
"@capacitor/local-notifications": "^8.3.1"
|
|
65
|
+
"@capacitor/local-notifications": "^8.3.1",
|
|
66
|
+
"@capawesome/capacitor-app-review": "^8.1.0"
|
|
57
67
|
},
|
|
58
68
|
"peerDependenciesMeta": {
|
|
59
69
|
"@capacitor-community/admob": {
|
|
60
70
|
"optional": true
|
|
61
71
|
},
|
|
72
|
+
"@capacitor-firebase/crashlytics": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
62
75
|
"@capacitor-firebase/messaging": {
|
|
63
76
|
"optional": true
|
|
64
77
|
},
|
|
65
78
|
"@capacitor/local-notifications": {
|
|
66
79
|
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"@capawesome/capacitor-app-review": {
|
|
82
|
+
"optional": true
|
|
67
83
|
}
|
|
68
84
|
},
|
|
69
85
|
"devDependencies": {
|
|
70
86
|
"@capacitor-community/admob": "8.1.0",
|
|
87
|
+
"@capacitor-firebase/crashlytics": "8.5.2",
|
|
71
88
|
"@capacitor-firebase/messaging": "8.5.2",
|
|
72
89
|
"@capacitor/core": "8.5.2",
|
|
73
90
|
"@capacitor/local-notifications": "8.3.1",
|
|
91
|
+
"@capawesome/capacitor-app-review": "8.1.0",
|
|
74
92
|
"ttsc": "0.32.1",
|
|
75
93
|
"typescript": "7.0.2",
|
|
76
94
|
"vitest": "^4.0.0",
|
|
77
|
-
"@mpgd/game-runtime": "0.8.
|
|
95
|
+
"@mpgd/game-runtime": "0.8.7"
|
|
78
96
|
},
|
|
79
97
|
"main": "./dist/index.js",
|
|
80
98
|
"types": "./dist/index.d.ts",
|