@planningcenter/topbar 11.2.5-qa-827.0 → 11.2.5-qa-832.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/topbar.js CHANGED
@@ -6,6 +6,7 @@ var pcoUrl$1 = require('@planningcenter/url');
6
6
  var general = require('@planningcenter/icons/paths/general');
7
7
  var Jolt = require('@planningcenter/jolt-client');
8
8
  var shim = require('use-sync-external-store/shim');
9
+ require('react/jsx-runtime');
9
10
  var tapestryReact = require('@planningcenter/tapestry-react');
10
11
  var addOns = require('@planningcenter/add-ons');
11
12
  var churchCenter = require('@planningcenter/icons/paths/church-center');
@@ -2248,10 +2249,750 @@ const sortByAttribute = attr => arr => arr.slice().sort((a, b) => a[attr].locale
2248
2249
  const appsMenuFormatter = apps => sortByAttribute("name")(apps).reduce((acc, item) => (item.name || "") === "Church Center" || (item.name || "") === "API" ? acc : item.name === "Home" ? [item, ...acc] : [...acc, item], []);
2249
2250
  const connectedPeopleMenuFormatter = connectedPeople => connectedPeople.slice().sort((a, b) => a.attributes["organization_name"].localeCompare(b.attributes["organization_name"]));
2250
2251
 
2252
+ // Generated by scripts/write-version.cjs from package.json. Do not edit.
2253
+ const BRIDGE_VERSION = "0.0.7-qa-41.0";
2254
+
2255
+ /**
2256
+ * PCA_BRIDGE_BUS_INIT — the script injected via
2257
+ * `injectedJavaScriptBeforeContentLoaded` before any web content loads.
2258
+ *
2259
+ * Installs `window.__pcaBridge` in the WebView's JS context. The bus:
2260
+ * - is per-WebView-context-by-construction (each WebView has its own JS
2261
+ * context; the global lives only in that context)
2262
+ * - accepts multiple registered handlers per reserved message type so
2263
+ * multiple Bridge instances in the same WebView can coexist
2264
+ * - buffers messages of a type with no registered handlers for up to 5s
2265
+ * - pings native with `__bridge/busReady` after install so the native
2266
+ * transport can flush its outgoing queue and trigger `reannounceLocal()`
2267
+ *
2268
+ * The bus runs as an IIFE so consumers can dispatch and register against it
2269
+ * immediately. Internal state (buffers, handlers) is closure-private.
2270
+ */
2271
+
2272
+ const PCA_BRIDGE_BUS_INIT = `
2273
+ (function () {
2274
+ if (typeof window === 'undefined') return
2275
+ if (window.__pcaBridge) return // idempotent in case it's injected twice
2276
+
2277
+ var BRIDGE_VERSION = ${JSON.stringify(BRIDGE_VERSION)}
2278
+ var BUFFER_TTL_MS = 5000
2279
+ var handlers = Object.create(null) // type -> Array<fn>
2280
+ var buffer = Object.create(null) // type -> Array<{msg, receivedAt}>
2281
+ var logger = {
2282
+ debug: function () {},
2283
+ warn: function (msg, ctx) {
2284
+ try { console.warn('[pca-bridge]', msg, ctx) } catch (e) {}
2285
+ },
2286
+ error: function (msg, ctx) {
2287
+ try { console.error('[pca-bridge]', msg, ctx) } catch (e) {}
2288
+ }
2289
+ }
2290
+
2291
+ function now() { return Date.now() }
2292
+
2293
+ function sweepBuffer() {
2294
+ var cutoff = now() - BUFFER_TTL_MS
2295
+ var dropped = []
2296
+ var hasAny = false
2297
+ for (var type in buffer) {
2298
+ var entries = buffer[type]
2299
+ var kept = []
2300
+ for (var i = 0; i < entries.length; i++) {
2301
+ if (entries[i].receivedAt >= cutoff) {
2302
+ kept.push(entries[i])
2303
+ } else {
2304
+ dropped.push({ type: type, ageMs: now() - entries[i].receivedAt })
2305
+ }
2306
+ }
2307
+ if (kept.length === 0) delete buffer[type]
2308
+ else {
2309
+ buffer[type] = kept
2310
+ hasAny = true
2311
+ }
2312
+ }
2313
+ if (dropped.length > 0 && window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
2314
+ try {
2315
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2316
+ type: '__bridge/drop',
2317
+ bridgeVersion: BRIDGE_VERSION,
2318
+ messages: dropped
2319
+ }))
2320
+ } catch (e) {}
2321
+ }
2322
+ // Stop ticking when the buffer drains; ensureSweep() restarts on demand.
2323
+ if (!hasAny && sweepInterval !== null) {
2324
+ clearInterval(sweepInterval)
2325
+ sweepInterval = null
2326
+ }
2327
+ }
2328
+
2329
+ var sweepInterval = null
2330
+ function ensureSweep() {
2331
+ if (sweepInterval !== null) return
2332
+ sweepInterval = setInterval(sweepBuffer, 1000)
2333
+ }
2334
+
2335
+ function register(type, fn) {
2336
+ if (!handlers[type]) handlers[type] = []
2337
+ handlers[type].push(fn)
2338
+ // Drain any buffered messages for this type, applying the same TTL the
2339
+ // background sweep uses so entries that already expired don't get
2340
+ // delivered just because sweep hasn't run yet.
2341
+ var pending = buffer[type]
2342
+ if (pending && pending.length > 0) {
2343
+ delete buffer[type]
2344
+ var cutoff = now() - BUFFER_TTL_MS
2345
+ for (var i = 0; i < pending.length; i++) {
2346
+ if (pending[i].receivedAt < cutoff) continue
2347
+ try { fn(pending[i].msg) } catch (e) {
2348
+ logger.error('Handler threw while draining buffer', { type: type, error: e })
2349
+ }
2350
+ }
2351
+ }
2352
+ }
2353
+
2354
+ function unregister(type, fn) {
2355
+ var arr = handlers[type]
2356
+ if (!arr) return
2357
+ var idx = arr.indexOf(fn)
2358
+ if (idx >= 0) arr.splice(idx, 1)
2359
+ if (arr.length === 0) delete handlers[type]
2360
+ }
2361
+
2362
+ function dispatch(msg) {
2363
+ if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return
2364
+ var type = msg.type
2365
+ var arr = handlers[type]
2366
+ if (arr && arr.length > 0) {
2367
+ // Snapshot the array to avoid issues if a handler registers/unregisters during dispatch
2368
+ var snapshot = arr.slice()
2369
+ for (var i = 0; i < snapshot.length; i++) {
2370
+ try { snapshot[i](msg) } catch (e) {
2371
+ logger.error('Handler threw during dispatch', { type: type, error: e })
2372
+ }
2373
+ }
2374
+ } else {
2375
+ if (!buffer[type]) buffer[type] = []
2376
+ buffer[type].push({ msg: msg, receivedAt: now() })
2377
+ ensureSweep()
2378
+ }
2379
+ }
2380
+
2381
+ function setLogger(next) {
2382
+ // Validate both methods the bus actually calls; a partial logger would
2383
+ // throw TypeError when dispatch/register tries to invoke a missing one.
2384
+ if (next && typeof next.warn === 'function' && typeof next.error === 'function') {
2385
+ logger = next
2386
+ }
2387
+ }
2388
+
2389
+ window.__pcaBridge = {
2390
+ register: register,
2391
+ unregister: unregister,
2392
+ dispatch: dispatch,
2393
+ setLogger: setLogger
2394
+ }
2395
+
2396
+ // Ping native: the bus is ready in this JS context. Native transport
2397
+ // listens for this to (a) flush its outgoing queue and (b) call
2398
+ // bridge.reannounceLocal() so the freshly-loaded web context learns about
2399
+ // already-exposed local actions/values.
2400
+ try {
2401
+ if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
2402
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: '__bridge/busReady', bridgeVersion: BRIDGE_VERSION }))
2403
+ }
2404
+ } catch (e) {}
2405
+ })();
2406
+ true;
2407
+ `;
2408
+
2409
+ class TimeoutError extends Error {
2410
+ constructor(name, timeoutMs) {
2411
+ super(`Call to "${name}" timed out after ${timeoutMs}ms`);
2412
+ this.name = 'TimeoutError';
2413
+ }
2414
+ }
2415
+ class HandlerError extends Error {
2416
+ constructor(name, code, originalMessage) {
2417
+ super(`Handler for "${name}" threw [${code}]: ${originalMessage}`);
2418
+ this.name = 'HandlerError';
2419
+ }
2420
+ }
2421
+ class DisposedError extends Error {
2422
+ constructor(name) {
2423
+ super(`Bridge disposed while call to "${name}" was in flight`);
2424
+ this.name = 'DisposedError';
2425
+ }
2426
+ }
2427
+
2428
+ let idCounter = 0;
2429
+ function generateId() {
2430
+ idCounter += 1;
2431
+ return `pca-bridge-${idCounter}`;
2432
+ }
2433
+ const noopLogger = {
2434
+ debug: () => {},
2435
+ warn: () => {},
2436
+ error: () => {}
2437
+ };
2438
+ function isIndexable(x) {
2439
+ return typeof x === 'object' && x !== null;
2440
+ }
2441
+
2442
+ /**
2443
+ * Shallow equality, one level deep. Used by `BridgeCore.publish` to dedup
2444
+ * re-emissions of new-but-content-equal values (e.g., a freshly-built
2445
+ * `modalButtons` array whose contents didn't change between renders).
2446
+ */
2447
+ function shallowEqual(a, b) {
2448
+ const sameRef = Object.is(a, b);
2449
+ const bothComparableObjects = isIndexable(a) && isIndexable(b) && Array.isArray(a) === Array.isArray(b);
2450
+ let result;
2451
+ if (sameRef) {
2452
+ result = true;
2453
+ } else if (!bothComparableObjects) {
2454
+ result = false;
2455
+ } else {
2456
+ const keys = Object.keys(a);
2457
+ result = keys.length === Object.keys(b).length && keys.every(k => Object.prototype.hasOwnProperty.call(b, k) && Object.is(a[k], b[k]));
2458
+ }
2459
+ return result;
2460
+ }
2461
+ function isWireEnvelope(parsed) {
2462
+ return typeof parsed === 'object' && parsed !== null && 'type' in parsed && typeof parsed.type === 'string' && parsed.type.startsWith('__bridge/') && 'bridgeVersion' in parsed && typeof parsed.bridgeVersion === 'string';
2463
+ }
2464
+ function parseWireEnvelope(rawData) {
2465
+ let parsed;
2466
+ try {
2467
+ parsed = JSON.parse(rawData);
2468
+ } catch {
2469
+ parsed = null;
2470
+ }
2471
+ return isWireEnvelope(parsed) ? parsed : null;
2472
+ }
2473
+
2474
+ const DEFAULT_TIMEOUT_MS = 5000;
2475
+ const INCOMING_CALL_QUEUE_TTL_MS = 5000;
2476
+ const INCOMING_CALL_QUEUE_MAX_PER_ACTION = 32;
2477
+ class BridgeCore {
2478
+ injectedJavaScriptBeforeContentLoaded = PCA_BRIDGE_BUS_INIT;
2479
+ localVersion = BRIDGE_VERSION;
2480
+ // `never` arg type lets us widen any user-defined `LocalActions[K]` callable
2481
+ // with a single `as` cast inside `expose()`.
2482
+ localActions = new Map();
2483
+ localValues = new Map();
2484
+ remoteActions = new Set();
2485
+ remoteValues = new Map();
2486
+ actionListeners = new Map();
2487
+ valueListeners = new Map();
2488
+ pendingCalls = new Map();
2489
+ callQueue = new Map();
2490
+
2491
+ // Private backing field + public getter so the `readonly` declared on the
2492
+ // Bridge interface is actually enforced — not just at the interface level
2493
+ // but for any code holding a concrete BridgeCore reference.
2494
+ _remoteVersion = undefined;
2495
+ get remoteVersion() {
2496
+ return this._remoteVersion;
2497
+ }
2498
+ disposed = false;
2499
+ constructor(options) {
2500
+ const sendRaw = options.send;
2501
+ this.send = msg => {
2502
+ sendRaw({
2503
+ ...msg,
2504
+ bridgeVersion: BRIDGE_VERSION
2505
+ });
2506
+ };
2507
+ this.logger = options.logger ?? noopLogger;
2508
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
2509
+ this.now = options.now ?? Date.now;
2510
+ }
2511
+ call(name, args, options) {
2512
+ const requestId = generateId();
2513
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
2514
+ return new Promise((resolve, reject) => {
2515
+ const timer = setTimeout(() => {
2516
+ if (this.pendingCalls.delete(requestId)) {
2517
+ reject(new TimeoutError(name, timeoutMs));
2518
+ }
2519
+ }, timeoutMs);
2520
+ // Heterogeneous map: cast the typed resolve to the loose storage slot.
2521
+ this.pendingCalls.set(requestId, {
2522
+ name,
2523
+ resolve: resolve,
2524
+ reject,
2525
+ timer
2526
+ });
2527
+ this.send({
2528
+ type: '__bridge/call',
2529
+ requestId,
2530
+ name,
2531
+ args
2532
+ });
2533
+ });
2534
+ }
2535
+ expose(name, handler) {
2536
+ if (this.localActions.has(name)) {
2537
+ this.logger.warn('Action already exposed; last-write-wins (the previous handler is gone)', {
2538
+ name
2539
+ });
2540
+ }
2541
+ this.localActions.set(name, handler);
2542
+ this.send({
2543
+ type: '__bridge/exposed',
2544
+ name,
2545
+ kind: 'action'
2546
+ });
2547
+ const queued = this.callQueue.get(name);
2548
+ if (queued) {
2549
+ this.callQueue.delete(name);
2550
+ const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
2551
+ for (const entry of queued) {
2552
+ if (entry.receivedAt >= cutoff) {
2553
+ void this.invokeLocalHandler(entry.msg);
2554
+ } else {
2555
+ this.logger.warn('Queued incoming call expired before handler registered', {
2556
+ name,
2557
+ ageMs: this.now() - entry.receivedAt
2558
+ });
2559
+ this.rejectQueued(entry, 'expired');
2560
+ }
2561
+ }
2562
+ }
2563
+ }
2564
+ unexpose(name) {
2565
+ if (this.localActions.delete(name)) {
2566
+ this.send({
2567
+ type: '__bridge/unexposed',
2568
+ name,
2569
+ kind: 'action'
2570
+ });
2571
+ }
2572
+ }
2573
+ hasRemoteAction(name) {
2574
+ return this.remoteActions.has(name);
2575
+ }
2576
+ onRemoteAction(name, callback) {
2577
+ let set = this.actionListeners.get(name);
2578
+ if (!set) {
2579
+ set = new Set();
2580
+ this.actionListeners.set(name, set);
2581
+ }
2582
+ set.add(callback);
2583
+ return () => {
2584
+ const current = this.actionListeners.get(name);
2585
+ if (current) {
2586
+ current.delete(callback);
2587
+ if (current.size === 0) {
2588
+ this.actionListeners.delete(name);
2589
+ }
2590
+ }
2591
+ };
2592
+ }
2593
+ publish(name, value) {
2594
+ // Skip wire send if content unchanged — callers may publish on every
2595
+ // render with a fresh-but-content-equal value.
2596
+ if (!shallowEqual(this.localValues.get(name), value)) {
2597
+ this.localValues.set(name, value);
2598
+ this.send({
2599
+ type: '__bridge/publish',
2600
+ name,
2601
+ value
2602
+ });
2603
+ }
2604
+ }
2605
+ unpublish(name) {
2606
+ if (this.localValues.delete(name)) {
2607
+ this.send({
2608
+ type: '__bridge/unexposed',
2609
+ name,
2610
+ kind: 'value'
2611
+ });
2612
+ }
2613
+ }
2614
+ seedLocalValue(name, value) {
2615
+ this.localValues.set(name, value);
2616
+ }
2617
+ getRemoteValue(name) {
2618
+ return this.remoteValues.get(name);
2619
+ }
2620
+ onRemoteValue(name, callback) {
2621
+ let set = this.valueListeners.get(name);
2622
+ if (!set) {
2623
+ set = new Set();
2624
+ this.valueListeners.set(name, set);
2625
+ }
2626
+ const wrapped = callback;
2627
+ set.add(wrapped);
2628
+ return () => {
2629
+ const current = this.valueListeners.get(name);
2630
+ if (current) {
2631
+ current.delete(wrapped);
2632
+ if (current.size === 0) {
2633
+ this.valueListeners.delete(name);
2634
+ }
2635
+ }
2636
+ };
2637
+ }
2638
+ reannounceLocal() {
2639
+ for (const name of this.localActions.keys()) {
2640
+ this.send({
2641
+ type: '__bridge/exposed',
2642
+ name,
2643
+ kind: 'action'
2644
+ });
2645
+ }
2646
+ for (const [name, value] of this.localValues) {
2647
+ this.send({
2648
+ type: '__bridge/exposed',
2649
+ name,
2650
+ kind: 'value',
2651
+ currentValue: value
2652
+ });
2653
+ }
2654
+ }
2655
+
2656
+ /**
2657
+ * Drop remote-side cached state. Called by transports when the remote JS
2658
+ * context is about to be torn down (e.g., WebView reload): the new context
2659
+ * will re-announce what it actually exposes, and stale entries would
2660
+ * otherwise silently mask removed actions/values.
2661
+ */
2662
+ clearRemoteState() {
2663
+ for (const name of this.remoteActions) {
2664
+ this.notifySet(this.actionListeners, name, false, 'action availability');
2665
+ }
2666
+ this.remoteActions.clear();
2667
+ for (const name of this.remoteValues.keys()) {
2668
+ this.notifySet(this.valueListeners, name, undefined, 'remote value change');
2669
+ }
2670
+ this.remoteValues.clear();
2671
+ this._remoteVersion = undefined;
2672
+ }
2673
+ handleMessage(rawData) {
2674
+ const msg = parseWireEnvelope(rawData);
2675
+ let consumed = false;
2676
+ if (msg) {
2677
+ this.handleIncomingMessage(msg);
2678
+ consumed = true;
2679
+ }
2680
+ return consumed;
2681
+ }
2682
+ applyLogger(logger) {
2683
+ if (this.logger === noopLogger) {
2684
+ this.logger = logger;
2685
+ return true;
2686
+ }
2687
+ return false;
2688
+ }
2689
+ handleLoadStart() {}
2690
+ handleLoadEnd() {}
2691
+ handleNavigationStateChange(navState) {
2692
+ if (!navState.loading) {
2693
+ this.handleLoadEnd();
2694
+ }
2695
+ }
2696
+ handleIncomingMessage(msg) {
2697
+ if (!this.disposed) {
2698
+ this._remoteVersion = msg.bridgeVersion;
2699
+ switch (msg.type) {
2700
+ case '__bridge/call':
2701
+ void this.routeIncomingCall(msg);
2702
+ break;
2703
+ case '__bridge/response':
2704
+ this.handleIncomingResponse(msg);
2705
+ break;
2706
+ case '__bridge/exposed':
2707
+ this.handleIncomingExposed(msg);
2708
+ break;
2709
+ case '__bridge/unexposed':
2710
+ this.handleIncomingUnexposed(msg);
2711
+ break;
2712
+ case '__bridge/publish':
2713
+ this.handleIncomingPublish(msg);
2714
+ break;
2715
+ case '__bridge/busReady':
2716
+ case '__bridge/drop':
2717
+ break;
2718
+ default:
2719
+ {
2720
+ // parseWireEnvelope admits any `__bridge/*` type; this catches
2721
+ // future message types a remote on a newer bridge sends to us.
2722
+ const unknown = msg;
2723
+ this.logger.warn('Received unknown bridge message type; ignoring', {
2724
+ type: unknown.type,
2725
+ remoteVersion: this.remoteVersion
2726
+ });
2727
+ break;
2728
+ }
2729
+ }
2730
+ }
2731
+ }
2732
+ get debug() {
2733
+ return {
2734
+ listLocalActions: () => Array.from(this.localActions.keys()),
2735
+ listRemoteActions: () => Array.from(this.remoteActions),
2736
+ listLocalValues: () => Array.from(this.localValues.keys()),
2737
+ listRemoteValues: () => Array.from(this.remoteValues.keys())
2738
+ };
2739
+ }
2740
+ dispose() {
2741
+ if (!this.disposed) {
2742
+ this.disposed = true;
2743
+ for (const pending of this.pendingCalls.values()) {
2744
+ clearTimeout(pending.timer);
2745
+ pending.reject(new DisposedError(pending.name));
2746
+ }
2747
+ this.pendingCalls.clear();
2748
+ this.callQueue.clear();
2749
+ }
2750
+ }
2751
+ async routeIncomingCall(msg) {
2752
+ if (this.localActions.has(msg.name)) {
2753
+ await this.invokeLocalHandler(msg);
2754
+ return;
2755
+ }
2756
+ let queued = this.callQueue.get(msg.name);
2757
+ if (!queued) {
2758
+ queued = [];
2759
+ this.callQueue.set(msg.name, queued);
2760
+ }
2761
+ // Evict TTL-expired and over-capacity entries so a remote that spams an
2762
+ // unknown action can't grow the queue without bound. Each evicted call
2763
+ // gets an error response so the caller fails fast.
2764
+ const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
2765
+ while (queued.length > 0) {
2766
+ const head = queued[0];
2767
+ if (!head || head.receivedAt >= cutoff) break;
2768
+ queued.shift();
2769
+ this.rejectQueued(head, 'expired');
2770
+ }
2771
+ while (queued.length >= INCOMING_CALL_QUEUE_MAX_PER_ACTION) {
2772
+ const evicted = queued.shift();
2773
+ if (!evicted) break;
2774
+ this.rejectQueued(evicted, 'queue_full');
2775
+ }
2776
+ queued.push({
2777
+ msg,
2778
+ receivedAt: this.now()
2779
+ });
2780
+ }
2781
+ rejectQueued(entry, code) {
2782
+ this.send({
2783
+ type: '__bridge/response',
2784
+ requestId: entry.msg.requestId,
2785
+ ok: false,
2786
+ code,
2787
+ message: `Queued call to "${entry.msg.name}" dropped (${code})`
2788
+ });
2789
+ }
2790
+ async invokeLocalHandler(msg) {
2791
+ // Handler may have been unregistered between queue push and drain. Reply
2792
+ // with an explicit error so the caller fails fast instead of timing out.
2793
+ const handler = this.localActions.get(msg.name);
2794
+ if (!handler) {
2795
+ this.send({
2796
+ type: '__bridge/response',
2797
+ requestId: msg.requestId,
2798
+ ok: false,
2799
+ code: 'unexposed',
2800
+ message: `No handler exposed for "${msg.name}"`
2801
+ });
2802
+ return;
2803
+ }
2804
+ try {
2805
+ const result = await handler(msg.args);
2806
+ this.send({
2807
+ type: '__bridge/response',
2808
+ requestId: msg.requestId,
2809
+ ok: true,
2810
+ result
2811
+ });
2812
+ } catch (error) {
2813
+ this.send({
2814
+ type: '__bridge/response',
2815
+ requestId: msg.requestId,
2816
+ ok: false,
2817
+ code: 'thrown',
2818
+ message: error instanceof Error ? error.message : String(error)
2819
+ });
2820
+ }
2821
+ }
2822
+ handleIncomingResponse(msg) {
2823
+ const pending = this.pendingCalls.get(msg.requestId);
2824
+ if (pending) {
2825
+ this.pendingCalls.delete(msg.requestId);
2826
+ clearTimeout(pending.timer);
2827
+ if (msg.ok) {
2828
+ pending.resolve(msg.result);
2829
+ } else {
2830
+ pending.reject(new HandlerError(pending.name, msg.code, msg.message));
2831
+ }
2832
+ } else {
2833
+ this.logger.warn('Response for unknown or already-resolved requestId', {
2834
+ requestId: msg.requestId
2835
+ });
2836
+ }
2837
+ }
2838
+ handleIncomingExposed(msg) {
2839
+ if (msg.kind === 'action') {
2840
+ if (!this.remoteActions.has(msg.name)) {
2841
+ this.remoteActions.add(msg.name);
2842
+ this.notifySet(this.actionListeners, msg.name, true, 'action availability');
2843
+ }
2844
+ } else {
2845
+ this.remoteValues.set(msg.name, msg.currentValue);
2846
+ this.notifySet(this.valueListeners, msg.name, msg.currentValue, 'remote value change');
2847
+ }
2848
+ }
2849
+ handleIncomingUnexposed(msg) {
2850
+ if (msg.kind === 'action') {
2851
+ if (this.remoteActions.delete(msg.name)) {
2852
+ this.notifySet(this.actionListeners, msg.name, false, 'action availability');
2853
+ }
2854
+ } else if (this.remoteValues.delete(msg.name)) {
2855
+ this.notifySet(this.valueListeners, msg.name, undefined, 'remote value change');
2856
+ }
2857
+ }
2858
+ handleIncomingPublish(msg) {
2859
+ this.remoteValues.set(msg.name, msg.value);
2860
+ this.notifySet(this.valueListeners, msg.name, msg.value, 'remote value change');
2861
+ }
2862
+ notifySet(subs, name, value, label) {
2863
+ const set = subs.get(name);
2864
+ if (set) {
2865
+ for (const callback of set) {
2866
+ try {
2867
+ callback(value);
2868
+ } catch (error) {
2869
+ this.logger.error(`Subscriber threw during ${label} notification`, {
2870
+ name,
2871
+ error
2872
+ });
2873
+ }
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+
2879
+ // Subset of window.__pcaBridge that bridge_web.ts needs.
2880
+
2881
+ const INBOUND_TYPES = ['__bridge/call', '__bridge/response', '__bridge/exposed', '__bridge/unexposed', '__bridge/publish'];
2882
+ class WebBridge extends BridgeCore {
2883
+ constructor(options = {}) {
2884
+ const bus = window.__pcaBridge;
2885
+ super({
2886
+ send: msg => {
2887
+ if (window.ReactNativeWebView) {
2888
+ try {
2889
+ window.ReactNativeWebView.postMessage(JSON.stringify(msg));
2890
+ } catch (error) {
2891
+ options.logger?.error('WebBridge postMessage failed', {
2892
+ error
2893
+ });
2894
+ }
2895
+ }
2896
+ },
2897
+ logger: options.logger,
2898
+ defaultTimeoutMs: options.defaultTimeoutMs
2899
+ });
2900
+ this.bus = bus;
2901
+ this.handlerFn = msg => {
2902
+ this.handleIncomingMessage(msg);
2903
+ };
2904
+ if (bus) {
2905
+ if (options.logger) {
2906
+ bus.setLogger(options.logger);
2907
+ }
2908
+ for (const type of INBOUND_TYPES) {
2909
+ bus.register(type, this.handlerFn);
2910
+ }
2911
+ if (window.ReactNativeWebView) {
2912
+ try {
2913
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2914
+ type: '__bridge/busReady',
2915
+ bridgeVersion: BRIDGE_VERSION
2916
+ }));
2917
+ } catch (error) {
2918
+ options.logger?.error('WebBridge busReady post failed', {
2919
+ error
2920
+ });
2921
+ }
2922
+ }
2923
+ } else {
2924
+ options.logger?.warn('WebBridge constructed without window.__pcaBridge — bus init script may not have run');
2925
+ }
2926
+ }
2927
+ applyLogger(logger) {
2928
+ const applied = super.applyLogger(logger);
2929
+ if (applied && this.bus) {
2930
+ this.bus.setLogger(logger);
2931
+ }
2932
+ return applied;
2933
+ }
2934
+ dispose() {
2935
+ if (this.bus) {
2936
+ for (const type of INBOUND_TYPES) {
2937
+ this.bus.unregister(type, this.handlerFn);
2938
+ }
2939
+ }
2940
+ super.dispose();
2941
+ }
2942
+ }
2943
+ function getOrCreateWebBridge(options = {}) {
2944
+ if (window.__pcaWebBridge && options.logger) {
2945
+ window.__pcaWebBridge.applyLogger(options.logger);
2946
+ } else if (!window.__pcaWebBridge) {
2947
+ window.__pcaWebBridge = new WebBridge(options);
2948
+ }
2949
+ return window.__pcaWebBridge;
2950
+ }
2951
+
2952
+ const WebBridgeContext = /*#__PURE__*/React.createContext(null);
2953
+ function useWebBridge() {
2954
+ const ctxBridge = React.useContext(WebBridgeContext);
2955
+ if (ctxBridge) {
2956
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
2957
+ return ctxBridge;
2958
+ } else {
2959
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
2960
+ return getOrCreateWebBridge();
2961
+ }
2962
+ }
2963
+ function useBridgePublish(bridge, name, value) {
2964
+ // publish() dedups by shallow equality, so this effect is safe to re-run
2965
+ // on every value change without spamming wire messages.
2966
+ React.useEffect(() => {
2967
+ bridge.publish(name, value);
2968
+ }, [bridge, name, value]);
2969
+ React.useEffect(() => {
2970
+ return () => {
2971
+ bridge.unpublish(name);
2972
+ };
2973
+ }, [bridge, name]);
2974
+ }
2975
+
2251
2976
  const useSetCurrentRouteInfo = ({
2252
2977
  routes,
2253
2978
  app
2254
2979
  }) => {
2980
+ const bridge = useWebBridge();
2981
+ useBridgePublish(bridge, "routeInfo", {
2982
+ currentApp: app,
2983
+ currentRoutes: routes.map(({
2984
+ key,
2985
+ props: {
2986
+ to,
2987
+ href,
2988
+ active
2989
+ }
2990
+ }) => ({
2991
+ active,
2992
+ name: key,
2993
+ url: to !== null && to !== void 0 ? to : href
2994
+ }))
2995
+ });
2255
2996
  React.useEffect(() => {
2256
2997
  if (window.topbar) {
2257
2998
  window.topbar.routeInfo = {
@@ -3627,11 +4368,6 @@ const {
3627
4368
  LoadingIndicator: LoadingIndicator$1,
3628
4369
  StackView: StackView$1
3629
4370
  } = addOns.components;
3630
- function CalendlyWidget(props) {
3631
- return props.url ? /*#__PURE__*/React.createElement(reactCalendly.InlineWidget, {
3632
- ...props
3633
- }) : null;
3634
- }
3635
4371
  function HelpdeskComponent({
3636
4372
  appName,
3637
4373
  closeDrawer,
@@ -3700,69 +4436,14 @@ function HelpdeskComponent({
3700
4436
  spacing: 1
3701
4437
  }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
3702
4438
  additionalComponents: {
3703
- CalendlyWidget: {
3704
- component: CalendlyWidget,
3705
- properties: {
3706
- pageSettings: {
3707
- attribute: false
3708
- },
3709
- prefill: {
3710
- attribute: false
3711
- },
3712
- styles: {
3713
- attribute: false
3714
- },
3715
- url: {
3716
- type: String
3717
- }
3718
- }
3719
- },
3720
- DocumentationSearch: {
3721
- component: DocumentationSearch,
3722
- properties: {
3723
- currentAppName: {
3724
- type: String
3725
- },
3726
- indexName: {
3727
- type: String
3728
- }
3729
- }
3730
- },
3731
- DrawerHeader: {
3732
- component: DrawerHeader,
3733
- properties: {
3734
- closeDrawer: {
3735
- type: Function,
3736
- event: false
3737
- }
3738
- }
3739
- },
4439
+ // @ts-expect-error TODO: Relax type specification in add-ons-js to not require children
4440
+ CalendlyWidget: reactCalendly.InlineWidget,
4441
+ DocumentationSearch,
4442
+ DrawerHeader,
3740
4443
  DrawerHeading,
3741
- PicobotIframe: {
3742
- component: PicobotIframe,
3743
- properties: {
3744
- currentPath: {
3745
- attribute: false
3746
- },
3747
- onChangePath: {
3748
- type: Function,
3749
- event: false
3750
- },
3751
- onMessage: {
3752
- type: Function,
3753
- event: false
3754
- }
3755
- }
3756
- },
4444
+ PicobotIframe,
3757
4445
  ScrollContainer,
3758
- SwitchView: {
3759
- component: SwitchView,
3760
- properties: {
3761
- activeIndex: {
3762
- type: Number
3763
- }
3764
- }
3765
- }
4446
+ SwitchView
3766
4447
  },
3767
4448
  args: {
3768
4449
  appName,
@@ -4366,13 +5047,8 @@ const {
4366
5047
  function NavLinkWithBadge({
4367
5048
  children,
4368
5049
  badge,
4369
- borderBottomColor,
4370
- borderBottomStyle,
4371
- borderBottomWidth,
4372
- color,
4373
- fontSize,
4374
- fontWeight,
4375
- onClick
5050
+ onClick,
5051
+ ...rest
4376
5052
  }) {
4377
5053
  return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
4378
5054
  as: "button",
@@ -4382,14 +5058,7 @@ function NavLinkWithBadge({
4382
5058
  paddingBottom: 0.75,
4383
5059
  onClick: () => onClick(),
4384
5060
  spacing: 1,
4385
- ...{
4386
- borderBottomColor,
4387
- borderBottomStyle,
4388
- borderBottomWidth,
4389
- color,
4390
- fontSize,
4391
- fontWeight
4392
- }
5061
+ ...rest
4393
5062
  }, children, badge && /*#__PURE__*/React.createElement(tapestryReact.Badge, {
4394
5063
  size: "sm",
4395
5064
  fontWeight: "600",
@@ -4441,55 +5110,11 @@ function TasksComponent({
4441
5110
  spacing: 1
4442
5111
  }, /*#__PURE__*/React.createElement(DrawerHeading, null, "To do"), /*#__PURE__*/React.createElement(LoadingIndicator, null)))),
4443
5112
  additionalComponents: {
4444
- DrawerHeader: {
4445
- component: DrawerHeader,
4446
- properties: {
4447
- closeDrawer: {
4448
- event: false,
4449
- type: Function
4450
- }
4451
- }
4452
- },
5113
+ DrawerHeader,
4453
5114
  DrawerHeading,
4454
- NavLinkWithBadge: {
4455
- component: NavLinkWithBadge,
4456
- properties: {
4457
- badge: {
4458
- attribute: false
4459
- },
4460
- borderBottomColor: {
4461
- type: String
4462
- },
4463
- borderBottomStyle: {
4464
- type: String
4465
- },
4466
- borderBottomWidth: {
4467
- type: String
4468
- },
4469
- color: {
4470
- type: String
4471
- },
4472
- fontSize: {
4473
- attribute: false
4474
- },
4475
- fontWeight: {
4476
- attribute: false
4477
- },
4478
- onClick: {
4479
- event: false,
4480
- type: Function
4481
- }
4482
- }
4483
- },
5115
+ NavLinkWithBadge,
4484
5116
  ScrollContainer,
4485
- SwitchView: {
4486
- component: SwitchView,
4487
- properties: {
4488
- activeIndex: {
4489
- type: Number
4490
- }
4491
- }
4492
- }
5117
+ SwitchView
4493
5118
  },
4494
5119
  args: {
4495
5120
  closeDrawer,