@scalebun/react-native 1.0.1 → 1.0.3
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/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +170 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +160 -12
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +32 -6
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +7 -1
- package/ios/Ota/BundleDownloader.swift +182 -0
- package/ios/Ota/OtaSlotManager.swift +35 -9
- package/ios/Ota/ScaleBunOtaBridge.mm +9 -0
- package/ios/Ota/ScaleBunOtaEventsModule.swift +61 -0
- package/ios/Ota/ScaleBunOtaModule.swift +58 -5
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +22 -0
- package/lib/commonjs/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/commonjs/features/crash/CrashFeature.js +18 -0
- package/lib/commonjs/features/crash/CrashFeature.js.map +1 -1
- package/lib/commonjs/features/crash/rejectionHandler.js +123 -0
- package/lib/commonjs/features/crash/rejectionHandler.js.map +1 -0
- package/lib/commonjs/features/engage/EngagePromptProvider.js +4 -2
- package/lib/commonjs/features/engage/EngagePromptProvider.js.map +1 -1
- package/lib/commonjs/features/engage/engageThrottle.js +41 -10
- package/lib/commonjs/features/engage/engageThrottle.js.map +1 -1
- package/lib/commonjs/features/engage/engageTypes.js.map +1 -1
- package/lib/commonjs/features/navigation/AutoScreenDetector.js +53 -10
- package/lib/commonjs/features/navigation/AutoScreenDetector.js.map +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +515 -20
- package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/commonjs/features/ota/environment.js +172 -0
- package/lib/commonjs/features/ota/environment.js.map +1 -0
- package/lib/commonjs/features/ota/retry.js +96 -0
- package/lib/commonjs/features/ota/retry.js.map +1 -0
- package/lib/commonjs/features/ota/signature.js +115 -0
- package/lib/commonjs/features/ota/signature.js.map +1 -0
- package/lib/commonjs/features/session/BackendSessionAdapter.js +231 -26
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/public/ScaleBunErrorBoundary.js +79 -0
- package/lib/commonjs/public/ScaleBunErrorBoundary.js.map +1 -0
- package/lib/commonjs/specs/NativeScaleBunOta.js.map +1 -1
- package/lib/module/bootstrap/SDKBootstrapper.js +22 -0
- package/lib/module/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/module/features/crash/CrashFeature.js +18 -0
- package/lib/module/features/crash/CrashFeature.js.map +1 -1
- package/lib/module/features/crash/rejectionHandler.js +116 -0
- package/lib/module/features/crash/rejectionHandler.js.map +1 -0
- package/lib/module/features/engage/EngagePromptProvider.js +4 -2
- package/lib/module/features/engage/EngagePromptProvider.js.map +1 -1
- package/lib/module/features/engage/engageThrottle.js +41 -10
- package/lib/module/features/engage/engageThrottle.js.map +1 -1
- package/lib/module/features/engage/engageTypes.js.map +1 -1
- package/lib/module/features/navigation/AutoScreenDetector.js +53 -10
- package/lib/module/features/navigation/AutoScreenDetector.js.map +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +516 -21
- package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/module/features/ota/environment.js +165 -0
- package/lib/module/features/ota/environment.js.map +1 -0
- package/lib/module/features/ota/retry.js +87 -0
- package/lib/module/features/ota/retry.js.map +1 -0
- package/lib/module/features/ota/signature.js +109 -0
- package/lib/module/features/ota/signature.js.map +1 -0
- package/lib/module/features/session/BackendSessionAdapter.js +230 -26
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/public/ScaleBunErrorBoundary.js +70 -0
- package/lib/module/public/ScaleBunErrorBoundary.js.map +1 -0
- package/lib/module/specs/NativeScaleBunOta.js.map +1 -1
- package/lib/typescript/bootstrap/SDKBootstrapper.d.ts.map +1 -1
- package/lib/typescript/features/crash/CrashFeature.d.ts.map +1 -1
- package/lib/typescript/features/crash/rejectionHandler.d.ts +46 -0
- package/lib/typescript/features/crash/rejectionHandler.d.ts.map +1 -0
- package/lib/typescript/features/engage/EngagePromptProvider.d.ts.map +1 -1
- package/lib/typescript/features/engage/engageThrottle.d.ts +24 -4
- package/lib/typescript/features/engage/engageThrottle.d.ts.map +1 -1
- package/lib/typescript/features/engage/engageTypes.d.ts +6 -0
- package/lib/typescript/features/engage/engageTypes.d.ts.map +1 -1
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +9 -0
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts.map +1 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +67 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
- package/lib/typescript/features/ota/OtaTypes.d.ts +42 -0
- package/lib/typescript/features/ota/OtaTypes.d.ts.map +1 -1
- package/lib/typescript/features/ota/environment.d.ts +99 -0
- package/lib/typescript/features/ota/environment.d.ts.map +1 -0
- package/lib/typescript/features/ota/retry.d.ts +53 -0
- package/lib/typescript/features/ota/retry.d.ts.map +1 -0
- package/lib/typescript/features/ota/signature.d.ts +66 -0
- package/lib/typescript/features/ota/signature.d.ts.map +1 -0
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts +93 -2
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/public/ScaleBunErrorBoundary.d.ts +44 -0
- package/lib/typescript/public/ScaleBunErrorBoundary.d.ts.map +1 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +11 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/bootstrap/SDKBootstrapper.ts +24 -0
- package/src/features/crash/CrashFeature.ts +19 -0
- package/src/features/crash/rejectionHandler.ts +134 -0
- package/src/features/engage/EngagePromptProvider.tsx +4 -2
- package/src/features/engage/engageThrottle.ts +44 -6
- package/src/features/engage/engageTypes.ts +6 -0
- package/src/features/navigation/AutoScreenDetector.ts +59 -4
- package/src/features/ota/OtaOrchestrator.ts +594 -23
- package/src/features/ota/OtaTypes.ts +56 -0
- package/src/features/ota/environment.ts +199 -0
- package/src/features/ota/retry.ts +123 -0
- package/src/features/ota/signature.ts +123 -0
- package/src/features/session/BackendSessionAdapter.ts +233 -25
- package/src/index.ts +6 -0
- package/src/public/ScaleBunErrorBoundary.tsx +79 -0
- package/src/specs/NativeScaleBunOta.ts +12 -0
|
@@ -49,11 +49,24 @@ const STORAGE_PREFIX = 'scalebun_engage_throttle:';
|
|
|
49
49
|
*/
|
|
50
50
|
export class EngageThrottleStore {
|
|
51
51
|
private readonly backend: StorageBackend;
|
|
52
|
+
/**
|
|
53
|
+
* Shows in THIS session, backing `maxImpressionsPerSession`.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately in-memory rather than persisted: on mobile a session is an app run, and this
|
|
56
|
+
* store is constructed once per provider mount, so the map's lifetime already is the session's.
|
|
57
|
+
* Persisting it would outlive the session it is meant to bound.
|
|
58
|
+
*/
|
|
59
|
+
private readonly sessionShows = new Map<string, number>();
|
|
52
60
|
|
|
53
61
|
constructor(backend: StorageBackend = createStorageBackend()) {
|
|
54
62
|
this.backend = backend;
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/** How many times this surface has been shown in the current session. */
|
|
66
|
+
sessionShowCount(campaignId: string): number {
|
|
67
|
+
return this.sessionShows.get(campaignId) ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
private keyFor(campaignId: string): string {
|
|
58
71
|
return `${STORAGE_PREFIX}${campaignId}`;
|
|
59
72
|
}
|
|
@@ -130,15 +143,28 @@ export class EngageThrottleStore {
|
|
|
130
143
|
completed: fresh ? false : prev.completed,
|
|
131
144
|
revision: revision ?? prev.revision,
|
|
132
145
|
});
|
|
146
|
+
// A new revision is a new design: its session count starts over with its durable counters.
|
|
147
|
+
this.sessionShows.set(campaignId, fresh ? 1 : this.sessionShowCount(campaignId) + 1);
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Mark a campaign as completed (response submitted, or an in-app message closed) — a hard stop.
|
|
152
|
+
*
|
|
153
|
+
* The `revision` MUST be carried through. This wrote the record without one, which silently
|
|
154
|
+
* disabled the revision check in `passesThrottle` for the rest of that campaign's life on the
|
|
155
|
+
* device: that check only fires when BOTH the live config and the stored state carry a revision,
|
|
156
|
+
* so a state stamped `undefined` can never be invalidated. The result was the exact opposite of
|
|
157
|
+
* the web bug — once a user closed an in-app message, editing the campaign could never bring the
|
|
158
|
+
* new version back to them. Callers that know the served revision pass it; legacy callers that
|
|
159
|
+
* don't keep whatever was already stored rather than erasing it.
|
|
160
|
+
*/
|
|
161
|
+
markCompleted(campaignId: string, now: number = Date.now(), revision?: string): void {
|
|
137
162
|
const prev = this.get(campaignId);
|
|
138
163
|
this.write(campaignId, {
|
|
139
164
|
impressions: prev.impressions,
|
|
140
165
|
lastShownAt: prev.lastShownAt || now,
|
|
141
166
|
completed: true,
|
|
167
|
+
revision: revision ?? prev.revision,
|
|
142
168
|
});
|
|
143
169
|
}
|
|
144
170
|
}
|
|
@@ -168,6 +194,7 @@ function passesThrottle(
|
|
|
168
194
|
state: CampaignThrottleState,
|
|
169
195
|
now: number,
|
|
170
196
|
revision?: string,
|
|
197
|
+
sessionShows = 0,
|
|
171
198
|
): boolean {
|
|
172
199
|
// Revision invalidation: if the live config carries a newer revision than the one
|
|
173
200
|
// the persisted state was recorded against, the campaign's content/design changed —
|
|
@@ -192,6 +219,15 @@ function passesThrottle(
|
|
|
192
219
|
return false;
|
|
193
220
|
}
|
|
194
221
|
|
|
222
|
+
// maxImpressionsPerSession: cap within this app run, under any lifetime cap above.
|
|
223
|
+
if (
|
|
224
|
+
typeof throttle.maxImpressionsPerSession === 'number' &&
|
|
225
|
+
throttle.maxImpressionsPerSession > 0 &&
|
|
226
|
+
sessionShows >= throttle.maxImpressionsPerSession
|
|
227
|
+
) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
195
231
|
// cooldownHours: enforce a quiet window since the last impression.
|
|
196
232
|
if (typeof throttle.cooldownHours === 'number' && state.lastShownAt > 0) {
|
|
197
233
|
const elapsedMs = now - state.lastShownAt;
|
|
@@ -219,9 +255,10 @@ export function isCampaignEligible(
|
|
|
219
255
|
campaign: EngageCampaignConfig,
|
|
220
256
|
state: CampaignThrottleState,
|
|
221
257
|
now: number = Date.now(),
|
|
258
|
+
sessionShows = 0,
|
|
222
259
|
): boolean {
|
|
223
260
|
if (!campaign.enabled) return false;
|
|
224
|
-
return passesThrottle(campaign.id, campaign.throttle, state, now);
|
|
261
|
+
return passesThrottle(campaign.id, campaign.throttle, state, now, undefined, sessionShows);
|
|
225
262
|
}
|
|
226
263
|
|
|
227
264
|
/**
|
|
@@ -234,8 +271,9 @@ export function isInAppEligible(
|
|
|
234
271
|
message: InAppMessageConfig,
|
|
235
272
|
state: CampaignThrottleState,
|
|
236
273
|
now: number = Date.now(),
|
|
274
|
+
sessionShows = 0,
|
|
237
275
|
): boolean {
|
|
238
|
-
return passesThrottle(message.id, message.throttle, state, now, message.revision);
|
|
276
|
+
return passesThrottle(message.id, message.throttle, state, now, message.revision, sessionShows);
|
|
239
277
|
}
|
|
240
278
|
|
|
241
279
|
/**
|
|
@@ -292,7 +330,7 @@ export function selectCampaignToRender(
|
|
|
292
330
|
typeof campaign.triggerConfig?.seconds === 'number' &&
|
|
293
331
|
campaign.triggerConfig.seconds !== options.seconds
|
|
294
332
|
) continue;
|
|
295
|
-
if (isCampaignEligible(campaign, store.get(campaign.id), now)) {
|
|
333
|
+
if (isCampaignEligible(campaign, store.get(campaign.id), now, store.sessionShowCount(campaign.id))) {
|
|
296
334
|
return campaign;
|
|
297
335
|
}
|
|
298
336
|
}
|
|
@@ -342,7 +380,7 @@ export function selectInAppToRender(
|
|
|
342
380
|
typeof options.seconds === 'number' &&
|
|
343
381
|
message.delaySeconds !== options.seconds
|
|
344
382
|
) continue;
|
|
345
|
-
if (isInAppEligible(message, store.get(message.id), now)) {
|
|
383
|
+
if (isInAppEligible(message, store.get(message.id), now, store.sessionShowCount(message.id))) {
|
|
346
384
|
return message;
|
|
347
385
|
}
|
|
348
386
|
}
|
|
@@ -69,6 +69,12 @@ export interface Throttle {
|
|
|
69
69
|
showOnce?: boolean;
|
|
70
70
|
cooldownHours?: number;
|
|
71
71
|
maxImpressions?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Cap WITHIN one session (one app run), independent of the lifetime caps above. The in-app
|
|
74
|
+
* builder writes this on every campaign and `deriveInAppMirror` now carries it to the wire, so
|
|
75
|
+
* ignoring it here would be offering an authoring control that does nothing on this platform.
|
|
76
|
+
*/
|
|
77
|
+
maxImpressionsPerSession?: number;
|
|
72
78
|
samplePct?: number;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -56,6 +56,8 @@ export class AutoScreenDetector {
|
|
|
56
56
|
private _started = false;
|
|
57
57
|
private _lastEmittedScreen: string | null = null;
|
|
58
58
|
private _navDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
59
|
+
/** Whether the React Navigation exports were actually replaced — see _autoHookReactNavigation. */
|
|
60
|
+
private _autoHookInstalled = false;
|
|
59
61
|
|
|
60
62
|
/** Cooldown: after manual screen is set, suppress auto-detection for this duration */
|
|
61
63
|
private static MANUAL_COOLDOWN_MS = 2000;
|
|
@@ -330,6 +332,37 @@ export class AutoScreenDetector {
|
|
|
330
332
|
if (!reactNav || reactNav.__scalebunAutoHooked) return;
|
|
331
333
|
|
|
332
334
|
const detector = this;
|
|
335
|
+
/** How many entry points were verifiably replaced — 0 means detection is dead. */
|
|
336
|
+
let installed = 0;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Replace an export, on a module whose exports are GETTERS.
|
|
340
|
+
*
|
|
341
|
+
* `@react-navigation/native` ships ESM whose index is only re-exports, and every bundler
|
|
342
|
+
* compiles `export { X } from './X.js'` to an accessor with a getter and no setter. Plain
|
|
343
|
+
* assignment to that throws in strict mode — which is what the three patch points here
|
|
344
|
+
* used to do, inside a `catch` that discarded the error. The hook reported success, no
|
|
345
|
+
* container ref was ever captured, and the whole screen-name pipeline stayed empty.
|
|
346
|
+
*
|
|
347
|
+
* Defining the property works where assigning does not, and the read-back is what makes
|
|
348
|
+
* a future failure visible instead of silent.
|
|
349
|
+
*/
|
|
350
|
+
const redefine = (key: string, value: unknown): boolean => {
|
|
351
|
+
try {
|
|
352
|
+
Object.defineProperty(reactNav, key, {
|
|
353
|
+
value,
|
|
354
|
+
configurable: true,
|
|
355
|
+
enumerable: true,
|
|
356
|
+
writable: true,
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
try {
|
|
360
|
+
reactNav[key] = value;
|
|
361
|
+
} catch { /* genuinely frozen — reported by the read-back below */ }
|
|
362
|
+
}
|
|
363
|
+
return reactNav[key] === value;
|
|
364
|
+
};
|
|
365
|
+
|
|
333
366
|
const register = (ref: any) => {
|
|
334
367
|
try {
|
|
335
368
|
const container = ref?.current ?? ref;
|
|
@@ -362,7 +395,7 @@ export class AutoScreenDetector {
|
|
|
362
395
|
return React.createElement(Original, { ...props, ref: setRef });
|
|
363
396
|
});
|
|
364
397
|
(Wrapped as any).displayName = 'ScaleBunNavigationContainer';
|
|
365
|
-
|
|
398
|
+
if (redefine('NavigationContainer', Wrapped)) installed++;
|
|
366
399
|
}
|
|
367
400
|
|
|
368
401
|
// 2. createNavigationContainerRef — apps that build the ref manually.
|
|
@@ -373,7 +406,7 @@ export class AutoScreenDetector {
|
|
|
373
406
|
try { detector.setNavigationRef(ref); } catch { /* no-throw */ }
|
|
374
407
|
return ref;
|
|
375
408
|
};
|
|
376
|
-
|
|
409
|
+
if (redefine('createNavigationContainerRef', patchedCreate)) installed++;
|
|
377
410
|
}
|
|
378
411
|
|
|
379
412
|
// 3. useNavigationContainerRef — hook variant of (2).
|
|
@@ -384,14 +417,36 @@ export class AutoScreenDetector {
|
|
|
384
417
|
try { detector.setNavigationRef(ref); } catch { /* no-throw */ }
|
|
385
418
|
return ref;
|
|
386
419
|
};
|
|
387
|
-
|
|
420
|
+
if (redefine('useNavigationContainerRef', patchedHook)) installed++;
|
|
388
421
|
}
|
|
389
422
|
|
|
423
|
+
this._autoHookInstalled = installed > 0;
|
|
390
424
|
try { reactNav.__scalebunAutoHooked = true; } catch { /* frozen — best-effort */ }
|
|
391
|
-
|
|
425
|
+
|
|
426
|
+
if (installed === 0) {
|
|
427
|
+
// Said out loud. The previous silence is why this shipped broken: an app with screen
|
|
428
|
+
// detection completely dead looked identical to one with it working.
|
|
429
|
+
logger.warn(
|
|
430
|
+
'[AutoScreenDetector] React Navigation is installed but none of its exports could be ' +
|
|
431
|
+
'wrapped — screens will not be detected automatically. Pass your container ref to ' +
|
|
432
|
+
'ScaleBun.setNavigationRef(ref) to restore screen names.',
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
logger.debug(`[AutoScreenDetector] React Navigation auto-hook installed (${installed}/3 entry points)`);
|
|
436
|
+
}
|
|
392
437
|
} catch { /* @react-navigation/native not installed — heuristic remains fallback */ }
|
|
393
438
|
}
|
|
394
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Did the zero-config hook actually take effect?
|
|
442
|
+
*
|
|
443
|
+
* Exposed because "we tried" and "it worked" were previously the same observable state, and the
|
|
444
|
+
* difference between them is every screen name in the product.
|
|
445
|
+
*/
|
|
446
|
+
isAutoHookInstalled(): boolean {
|
|
447
|
+
return this._autoHookInstalled;
|
|
448
|
+
}
|
|
449
|
+
|
|
395
450
|
/** Stop auto-detection and clean up */
|
|
396
451
|
stop(): void {
|
|
397
452
|
if (this._navDebounceTimer) {
|