@planningcenter/topbar 11.2.4 → 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 = {
@@ -3466,12 +4207,16 @@ var styles = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5",
3466
4207
 
3467
4208
  const pcoUrl = pcoUrl$1;
3468
4209
 
4210
+ // The route rides in the iframe URL fragment (`…/topbar#conversations/abc`),
4211
+ // not the path, so updating it is a same-document navigation that doesn't
4212
+ // reload the iframe, but also lets the route survive a forced reload (e.g.
4213
+ // Turbo re-parenting the topbar).
3469
4214
  function buildPicobotSrc(origin, path) {
3470
4215
  const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
3471
4216
  const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
3472
- const pathWithPrefix = ["topbar", safePath].filter(Boolean).join("/");
3473
- const relativePath = queryPart !== undefined ? `${pathWithPrefix}?${queryPart}` : pathWithPrefix;
3474
- return new URL(relativePath, origin).toString();
4217
+ const fragment = queryPart !== undefined ? `${safePath}?${queryPart}` : safePath;
4218
+ const base = new URL("topbar", origin).toString();
4219
+ return fragment ? `${base}#${fragment}` : base;
3475
4220
  }
3476
4221
 
3477
4222
  const handleSearchSelect = (hit, indexName) => {
@@ -3639,13 +4384,10 @@ function HelpdeskComponent({
3639
4384
  onMessage
3640
4385
  }) {
3641
4386
  const ref = React.useRef(null);
3642
- const initialPath = iframePath || "";
3643
- // Tracks the path picobot last reported via postMessage so an
3644
- // incoming currentPath that matches it doesn't trigger a reload.
3645
- // Picobot sends a bare relative path (e.g. "conversations/abc") with
3646
- // no leading slash or /topbar prefix—buildPicobotSrc adds those.
3647
- const lastReportedPathRef = React.useRef(initialPath);
3648
- const [loadedPath, setLoadedPath] = React.useState(initialPath);
4387
+ // Keep the fragment in sync in both directions—external opens (deeplink /
4388
+ // Contact support) and picobot's own NEW_ROUTE_RENDERED reports—so it
4389
+ // always reflects the live conversation. See buildPicobotSrc.
4390
+ const [loadedPath, setLoadedPath] = React.useState(iframePath || "");
3649
4391
  React.useEffect(() => {
3650
4392
  if (!onIframeChangePath && !onMessage) return undefined;
3651
4393
  const handler = e => {
@@ -3655,7 +4397,7 @@ function HelpdeskComponent({
3655
4397
  if (e.source !== ref.current.contentWindow) return;
3656
4398
  if (((_a = e.data) === null || _a === void 0 ? void 0 : _a.type) === "NEW_ROUTE_RENDERED" && onIframeChangePath) {
3657
4399
  const reportedPath = String((_b = e.data.payload) !== null && _b !== void 0 ? _b : "");
3658
- lastReportedPathRef.current = reportedPath;
4400
+ setLoadedPath(reportedPath);
3659
4401
  onIframeChangePath(reportedPath);
3660
4402
  }
3661
4403
  if (onMessage) onMessage(e.data);
@@ -3664,9 +4406,7 @@ function HelpdeskComponent({
3664
4406
  return () => window.removeEventListener("message", handler);
3665
4407
  }, [onIframeChangePath, onMessage]);
3666
4408
  React.useEffect(() => {
3667
- const incomingPath = iframePath || "";
3668
- if (lastReportedPathRef.current === incomingPath) return;
3669
- setLoadedPath(incomingPath);
4409
+ setLoadedPath(iframePath || "");
3670
4410
  }, [iframePath]);
3671
4411
  const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
3672
4412
  return /*#__PURE__*/React.createElement("iframe", {
@@ -3721,7 +4461,8 @@ const Helpdesk = {
3721
4461
  staticDotPath: "",
3722
4462
  staticPath: "M16 10C14.9391 10 13.9217 10.4214 13.1716 11.1716C12.4214 11.9217 12 12.9391 12 14H14C14 13.4696 14.2107 12.9609 14.5858 12.5858C14.9609 12.2107 15.4696 12 16 12C16.5304 12 17.0391 12.2107 17.4142 12.5858C17.7893 12.9609 18 13.4696 18 14C18 16 15 15.75 15 19H17C17 16.75 20 16.5 20 14C20 12.9391 19.5786 11.9217 18.8284 11.1716C18.0783 10.4214 17.0609 10 16 10ZM16 24C14.4178 24 12.871 23.5308 11.5554 22.6518C10.2399 21.7727 9.21447 20.5233 8.60897 19.0615C8.00347 17.5997 7.84504 15.9911 8.15372 14.4393C8.4624 12.8874 9.22433 11.462 10.3432 10.3431C11.462 9.22433 12.8874 8.4624 14.4393 8.15372C15.9911 7.84504 17.5997 8.00346 19.0615 8.60896C20.5233 9.21446 21.7727 10.2398 22.6518 11.5554C23.5308 12.871 24 14.4177 24 16C23.9974 18.1209 23.1537 20.1542 21.6539 21.6539C20.1542 23.1537 18.1209 23.9974 16 24ZM16 6C14.0222 6 12.0888 6.58649 10.4443 7.6853C8.79981 8.78412 7.51809 10.3459 6.76121 12.1732C6.00433 14.0004 5.8063 16.0111 6.19215 17.9509C6.578 19.8907 7.53041 21.6725 8.92894 23.0711C10.3275 24.4696 12.1093 25.422 14.0491 25.8079C15.9889 26.1937 17.9996 25.9957 19.8268 25.2388C21.6541 24.4819 23.2159 23.2002 24.3147 21.5557C25.4135 19.9112 26 17.9778 26 16C26 13.3478 24.9464 10.8043 23.0711 8.92893C21.1957 7.05357 18.6522 6 16 6V6ZM15 22H17V20H15V22Z"
3723
4463
  },
3724
- name: "Helpdesk"
4464
+ name: "Helpdesk",
4465
+ storageExemptPaths: ["email", "phone"]
3725
4466
  };
3726
4467
 
3727
4468
  const Eyebrow = ({
@@ -4652,6 +5393,13 @@ function isToolPathExemptFromStorage(toolName, path) {
4652
5393
  const tool = toolsRouteMap[toolName];
4653
5394
  return (_b = (_a = tool === null || tool === void 0 ? void 0 : tool.storageExemptPaths) === null || _a === void 0 ? void 0 : _a.includes(path)) !== null && _b !== void 0 ? _b : false;
4654
5395
  }
5396
+ function scrollToolDrawerToTop(toolName) {
5397
+ var _a;
5398
+ (_a = document.getElementById(`toolbarDrawer${toolName}`)) === null || _a === void 0 ? void 0 : _a.scrollTo({
5399
+ behavior: "smooth",
5400
+ top: 0
5401
+ });
5402
+ }
4655
5403
  const TOOLS_MAP = {
4656
5404
  Chat,
4657
5405
  Helpdesk,
@@ -4721,17 +5469,21 @@ function Toolbar({
4721
5469
  unsubscribe();
4722
5470
  };
4723
5471
  }, [open, clearRoute]);
5472
+ const openHelpdeskAtRoot = React.useCallback(() => {
5473
+ onChangeToolbarFragmentRoute(Helpdesk.name.toLowerCase(), "");
5474
+ scrollToolDrawerToTop(Helpdesk.name);
5475
+ }, [onChangeToolbarFragmentRoute]);
4724
5476
  React.useEffect(() => {
4725
5477
  window.topbar = window.topbar || {};
4726
5478
  window.topbar.openProfile = () => handleToolOnClick(Profile);
4727
- window.topbar.openHelpdesk = () => handleToolOnClick(Helpdesk);
5479
+ window.topbar.openHelpdesk = openHelpdeskAtRoot;
4728
5480
  window.openHelpdesk = window.topbar.openHelpdesk; /* TODO: Remove when products have migrated to window.topbar.openHelpdesk */
4729
5481
  return () => {
4730
5482
  delete window.topbar.openProfile;
4731
5483
  delete window.topbar.openHelpdesk;
4732
5484
  delete window.openHelpdesk; /* TODO: Remove when products have migrated to window.topbar.openHelpdesk */
4733
5485
  };
4734
- }, [Helpdesk, Profile]);
5486
+ }, [Profile, openHelpdeskAtRoot]);
4735
5487
  const loginSettingsUrl = getLoginProfilePath({
4736
5488
  appName: appName.replace(/[\s-]/, ""),
4737
5489
  env: env,
@@ -4813,12 +5565,8 @@ function Toolbar({
4813
5565
  });
4814
5566
  const TOOLS = [Notifications, Tasks, displayChatIcon && Chat].filter(Boolean);
4815
5567
  const handleToolOnClick = tool => {
4816
- var _a;
4817
5568
  toggleDrawer(tool);
4818
- (_a = document.getElementById(`toolbarDrawer${tool.name}`)) === null || _a === void 0 ? void 0 : _a.scrollTo({
4819
- behavior: "smooth",
4820
- top: 0
4821
- });
5569
+ scrollToolDrawerToTop(tool.name);
4822
5570
  };
4823
5571
  const toolSpecificProps = {
4824
5572
  Profile: {