@planningcenter/topbar 11.2.5 → 12.0.0-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');
@@ -964,8 +965,8 @@ const AppsProvider = ({
964
965
  const notificationsEnabled = !window.notificationsDisabled;
965
966
  const organizationTimeZone = ((_k = (_j = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _j === void 0 ? void 0 : _j.attributes) === null || _k === void 0 ? void 0 : _k.org_time_zone) || "America/Los_Angeles";
966
967
  const algoliaSearchAuth = {
967
- algoliaApplicationId: (_m = (_l = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _l === void 0 ? void 0 : _l.attributes) === null || _m === void 0 ? void 0 : _m.algolia_application_identifier,
968
- algoliaSearchOnlyApiKey: (_p = (_o = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _o === void 0 ? void 0 : _o.attributes) === null || _p === void 0 ? void 0 : _p.algolia_search_key
968
+ algoliaApplicationId: (_m = (_l = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _l === void 0 ? void 0 : _l.attributes) === null || _m === void 0 ? void 0 : _m.algolia_help_center_application_identifier,
969
+ algoliaSearchOnlyApiKey: (_p = (_o = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _o === void 0 ? void 0 : _o.attributes) === null || _p === void 0 ? void 0 : _p.algolia_help_center_search_key
969
970
  };
970
971
  const joltPromise = React.useMemo(() => {
971
972
  return resolveJoltPromise({
@@ -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 = {
@@ -2271,6 +3012,24 @@ const useSetCurrentRouteInfo = ({
2271
3012
  };
2272
3013
  }
2273
3014
  }, [routes, app]);
3015
+ // Publish info for when app goes back
3016
+ React.useEffect(() => window.addEventListener("pageshow", () => {
3017
+ bridge.publish("routeInfo", {
3018
+ currentApp: app,
3019
+ currentRoutes: routes.map(({
3020
+ key,
3021
+ props: {
3022
+ to,
3023
+ href,
3024
+ active
3025
+ }
3026
+ }) => ({
3027
+ active,
3028
+ name: key,
3029
+ url: to !== null && to !== void 0 ? to : href
3030
+ }))
3031
+ });
3032
+ }), []);
2274
3033
  };
2275
3034
 
2276
3035
  const useUpdateStore = () => React.useCallback(updates => {
@@ -3478,40 +4237,44 @@ function buildPicobotSrc(origin, path) {
3478
4237
  return fragment ? `${base}#${fragment}` : base;
3479
4238
  }
3480
4239
 
3481
- const handleSearchSelect = (hit, indexName) => {
3482
- const appName = indexName.split("_")[1];
3483
- const zendeskUrl = `https://${appName}.zendesk.com/hc/${hit.locale.locale}/articles/${hit.id}`;
3484
- const newTab = window.open(zendeskUrl, "_blank");
4240
+ // The article heading lives in the deepest populated hierarchy level (lvl0 is
4241
+ // always "Planning Center").
4242
+ const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
4243
+ const handleSearchSelect = hit => {
4244
+ // `url` is always absolute (it is also the basis of objectID), so open it
4245
+ // directly. Bail out defensively if a record somehow arrives without one.
4246
+ if (!(hit === null || hit === void 0 ? void 0 : hit.url)) return;
4247
+ const newTab = window.open(hit.url, "_blank");
3485
4248
  if (newTab) {
3486
4249
  newTab.focus();
3487
4250
  }
3488
4251
  };
4252
+ // Mirrors @planningcenter/tapestry-react's Logo registry.
4253
+ const VALID_LOGO_NAMES = new Set(["api", "calendar", "check-ins", "church-center", "giving", "groups", "headcounts", "home", "login", "music-stand", "people", "planning-center", "publishing", "registrations", "services"]);
3489
4254
  const HeadingIcon = ({
3490
- sectionName,
3491
- currentAppName
4255
+ currentAppName,
4256
+ product
3492
4257
  }) => {
3493
- const lowerCaseSectionName = sectionName.toLowerCase();
3494
- if (lowerCaseSectionName === "account information" || lowerCaseSectionName === "recent updates") {
3495
- return /*#__PURE__*/React.createElement(tapestryReact.Icon, {
3496
- name: lowerCaseSectionName === "account information" ? "general.cogPerson" : "general.outlinedGenericFile",
3497
- color: "foregroundSecondary"
3498
- });
3499
- } else if (currentAppName === "Accounts") {
4258
+ const appName = product || currentAppName;
4259
+ if (!appName) return null;
4260
+ const logoName = appName.toLowerCase().replace(/\s+/g, "-");
4261
+ // Accounts has no tapestry Logo, so use the cog icon
4262
+ if (logoName === "accounts") {
3500
4263
  return /*#__PURE__*/React.createElement(tapestryReact.Icon, {
3501
4264
  name: "general.cog",
3502
4265
  color: "foregroundSecondary"
3503
4266
  });
3504
- } else {
3505
- return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
3506
- width: 2,
3507
- height: 2
3508
- }, /*#__PURE__*/React.createElement(tapestryReact.Logo, {
3509
- name: currentAppName.toLowerCase().replace(/[\s]/, "-"),
3510
- size: "sm",
3511
- theme: "color",
3512
- markOnly: true
3513
- }));
3514
4267
  }
4268
+ if (!VALID_LOGO_NAMES.has(logoName)) return null;
4269
+ return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
4270
+ width: 2,
4271
+ height: 2
4272
+ }, /*#__PURE__*/React.createElement(tapestryReact.Logo, {
4273
+ name: logoName,
4274
+ size: "sm",
4275
+ theme: "color",
4276
+ markOnly: true
4277
+ }));
3515
4278
  };
3516
4279
  const HitResult = ({
3517
4280
  hit,
@@ -3526,11 +4289,10 @@ const HitResult = ({
3526
4289
  color: "neutralDarker",
3527
4290
  size: 4,
3528
4291
  whiteSpace: "normal"
3529
- }, hit.title));
4292
+ }, hitTitle(hit.hierarchy)));
3530
4293
  };
3531
4294
  const CustomSearchBox = ({
3532
- currentAppName,
3533
- indexName
4295
+ currentAppName
3534
4296
  }) => {
3535
4297
  const {
3536
4298
  query,
@@ -3541,7 +4303,10 @@ const CustomSearchBox = ({
3541
4303
  results
3542
4304
  } = reactInstantsearchCore.useHits();
3543
4305
  const groups = results === null || results === void 0 ? void 0 : results.hits.reduce((acc, hit) => {
3544
- const key = hit.category.title;
4306
+ // A result with no url can't be opened, so don't render it at all.
4307
+ if (!hit.url) return acc;
4308
+ // Fall back so a record without a product never yields an "undefined" heading.
4309
+ const key = hit.product || currentAppName || "Other";
3545
4310
  if (!acc[key]) {
3546
4311
  acc[key] = [];
3547
4312
  }
@@ -3563,7 +4328,7 @@ const CustomSearchBox = ({
3563
4328
  name: "general.search"
3564
4329
  }),
3565
4330
  onChange: e => setQuery(e.target.value),
3566
- onSelect: selectedValue => handleSearchSelect(selectedValue, indexName),
4331
+ onSelect: selectedValue => handleSearchSelect(selectedValue),
3567
4332
  placement: "bottom-start"
3568
4333
  }, !hits.length && (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
3569
4334
  axis: "horizontal",
@@ -3575,7 +4340,7 @@ const CustomSearchBox = ({
3575
4340
  size: 4,
3576
4341
  whiteSpace: "normal"
3577
4342
  }, "No results match your search")))), groups && Object.keys(groups).map(key => {
3578
- const hits = groups[key];
4343
+ const groupHits = groups[key];
3579
4344
  return /*#__PURE__*/React.createElement(React.Fragment, {
3580
4345
  key: key
3581
4346
  }, resultIndex > 0 && /*#__PURE__*/React.createElement(tapestryReact.Divider, {
@@ -3588,12 +4353,12 @@ const CustomSearchBox = ({
3588
4353
  paddingVertical: 1
3589
4354
  }, /*#__PURE__*/React.createElement(HeadingIcon, {
3590
4355
  currentAppName: currentAppName,
3591
- sectionName: key
4356
+ product: key
3592
4357
  }), /*#__PURE__*/React.createElement(tapestryReact.Text, {
3593
4358
  size: 5,
3594
4359
  color: "foregroundSecondary",
3595
4360
  fontWeight: 500
3596
- }, key)), hits.map(hit => (/*#__PURE__*/React.createElement(HitResult, {
4361
+ }, key)), groupHits.map(hit => (/*#__PURE__*/React.createElement(HitResult, {
3597
4362
  key: hit.objectID,
3598
4363
  hit: hit,
3599
4364
  index: resultIndex++
@@ -3601,8 +4366,8 @@ const CustomSearchBox = ({
3601
4366
  })));
3602
4367
  };
3603
4368
 
4369
+ const HELP_CENTER_INDEX = "help_center_english";
3604
4370
  const DocumentationSearch = ({
3605
- indexName,
3606
4371
  currentAppName
3607
4372
  }) => {
3608
4373
  const {
@@ -3613,13 +4378,13 @@ const DocumentationSearch = ({
3613
4378
  } = useStore();
3614
4379
  const searchClient = algoliasearch(algoliaApplicationId, algoliaSearchOnlyApiKey);
3615
4380
  return /*#__PURE__*/React.createElement(reactInstantsearchCore.InstantSearch, {
3616
- indexName: indexName,
4381
+ indexName: HELP_CENTER_INDEX,
3617
4382
  searchClient: searchClient
3618
4383
  }, /*#__PURE__*/React.createElement(reactInstantsearchCore.Configure, {
3619
- hitsPerPage: 5
4384
+ hitsPerPage: 5,
4385
+ optionalFilters: currentAppName ? [`product:${currentAppName}`] : undefined
3620
4386
  }), /*#__PURE__*/React.createElement(CustomSearchBox, {
3621
- currentAppName: currentAppName,
3622
- indexName: indexName
4387
+ currentAppName: currentAppName
3623
4388
  }));
3624
4389
  };
3625
4390
 
@@ -3627,6 +4392,19 @@ const {
3627
4392
  LoadingIndicator: LoadingIndicator$1,
3628
4393
  StackView: StackView$1
3629
4394
  } = addOns.components;
4395
+ function CalendlyWidget({
4396
+ url,
4397
+ pageSettings,
4398
+ prefill,
4399
+ styles
4400
+ }) {
4401
+ return url ? (/*#__PURE__*/React.createElement(reactCalendly.InlineWidget, {
4402
+ url: url,
4403
+ pageSettings: pageSettings,
4404
+ prefill: prefill,
4405
+ styles: styles
4406
+ })) : null;
4407
+ }
3630
4408
  function HelpdeskComponent({
3631
4409
  appName,
3632
4410
  closeDrawer,
@@ -3695,14 +4473,65 @@ function HelpdeskComponent({
3695
4473
  spacing: 1
3696
4474
  }, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
3697
4475
  additionalComponents: {
3698
- // @ts-expect-error TODO: Relax type specification in add-ons-js to not require children
3699
- CalendlyWidget: reactCalendly.InlineWidget,
3700
- DocumentationSearch,
3701
- DrawerHeader,
4476
+ CalendlyWidget: {
4477
+ component: CalendlyWidget,
4478
+ properties: {
4479
+ pageSettings: {
4480
+ attribute: false
4481
+ },
4482
+ prefill: {
4483
+ attribute: false
4484
+ },
4485
+ styles: {
4486
+ attribute: false
4487
+ },
4488
+ url: {
4489
+ type: String
4490
+ }
4491
+ }
4492
+ },
4493
+ DocumentationSearch: {
4494
+ component: DocumentationSearch,
4495
+ properties: {
4496
+ currentAppName: {
4497
+ type: String
4498
+ }
4499
+ }
4500
+ },
4501
+ DrawerHeader: {
4502
+ component: DrawerHeader,
4503
+ properties: {
4504
+ closeDrawer: {
4505
+ event: false
4506
+ }
4507
+ }
4508
+ },
3702
4509
  DrawerHeading,
3703
- PicobotIframe,
4510
+ PicobotIframe: {
4511
+ component: PicobotIframe,
4512
+ properties: {
4513
+ currentPath: {
4514
+ attribute: false
4515
+ },
4516
+ onChangePath: {
4517
+ event: false,
4518
+ type: Function
4519
+ },
4520
+ onMessage: {
4521
+ event: false,
4522
+ type: Function
4523
+ }
4524
+ }
4525
+ },
3704
4526
  ScrollContainer,
3705
- SwitchView
4527
+ SwitchView: {
4528
+ component: SwitchView,
4529
+ properties: {
4530
+ activeIndex: {
4531
+ type: Number
4532
+ }
4533
+ }
4534
+ }
3706
4535
  },
3707
4536
  args: {
3708
4537
  appName,
@@ -4306,8 +5135,13 @@ const {
4306
5135
  function NavLinkWithBadge({
4307
5136
  children,
4308
5137
  badge,
4309
- onClick,
4310
- ...rest
5138
+ borderBottomColor,
5139
+ borderBottomStyle,
5140
+ borderBottomWidth,
5141
+ color,
5142
+ fontSize,
5143
+ fontWeight,
5144
+ onClick
4311
5145
  }) {
4312
5146
  return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
4313
5147
  as: "button",
@@ -4317,7 +5151,14 @@ function NavLinkWithBadge({
4317
5151
  paddingBottom: 0.75,
4318
5152
  onClick: () => onClick(),
4319
5153
  spacing: 1,
4320
- ...rest
5154
+ ...{
5155
+ borderBottomColor,
5156
+ borderBottomStyle,
5157
+ borderBottomWidth,
5158
+ color,
5159
+ fontSize,
5160
+ fontWeight
5161
+ }
4321
5162
  }, children, badge && /*#__PURE__*/React.createElement(tapestryReact.Badge, {
4322
5163
  size: "sm",
4323
5164
  fontWeight: "600",
@@ -4369,11 +5210,55 @@ function TasksComponent({
4369
5210
  spacing: 1
4370
5211
  }, /*#__PURE__*/React.createElement(DrawerHeading, null, "To do"), /*#__PURE__*/React.createElement(LoadingIndicator, null)))),
4371
5212
  additionalComponents: {
4372
- DrawerHeader,
5213
+ DrawerHeader: {
5214
+ component: DrawerHeader,
5215
+ properties: {
5216
+ closeDrawer: {
5217
+ event: false,
5218
+ type: Function
5219
+ }
5220
+ }
5221
+ },
4373
5222
  DrawerHeading,
4374
- NavLinkWithBadge,
5223
+ NavLinkWithBadge: {
5224
+ component: NavLinkWithBadge,
5225
+ properties: {
5226
+ badge: {
5227
+ attribute: false
5228
+ },
5229
+ borderBottomColor: {
5230
+ type: String
5231
+ },
5232
+ borderBottomStyle: {
5233
+ type: String
5234
+ },
5235
+ borderBottomWidth: {
5236
+ type: String
5237
+ },
5238
+ color: {
5239
+ type: String
5240
+ },
5241
+ fontSize: {
5242
+ attribute: false
5243
+ },
5244
+ fontWeight: {
5245
+ attribute: false
5246
+ },
5247
+ onClick: {
5248
+ event: false,
5249
+ type: Function
5250
+ }
5251
+ }
5252
+ },
4375
5253
  ScrollContainer,
4376
- SwitchView
5254
+ SwitchView: {
5255
+ component: SwitchView,
5256
+ properties: {
5257
+ activeIndex: {
5258
+ type: Number
5259
+ }
5260
+ }
5261
+ }
4377
5262
  },
4378
5263
  args: {
4379
5264
  closeDrawer,