@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.
@@ -1,10 +1,11 @@
1
1
  import { Link } from '@planningcenter/tapestry';
2
2
  import * as React from 'react';
3
- import React__default, { useState, useEffect, useMemo, useLayoutEffect, useCallback, useRef } from 'react';
3
+ import React__default, { useState, useEffect, useMemo, useLayoutEffect, useContext, createContext, useCallback, useRef } from 'react';
4
4
  import pcoUrl$1 from '@planningcenter/url';
5
5
  import * as general from '@planningcenter/icons/paths/general';
6
6
  import Jolt from '@planningcenter/jolt-client';
7
7
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
8
+ import 'react/jsx-runtime';
8
9
  import { ThemeProvider, Divider, Button, StackView as StackView$2, Text, Icon, designSystem, Box, Heading, GridView, Avatar as Avatar$1, Link as Link$1, HeadingUppercase, List, Combobox, Logo, Dropdown, Spinner, Badge } from '@planningcenter/tapestry-react';
9
10
  import { components, UiExtensionFirstPartyComponent, UiExtensionDataContext } from '@planningcenter/add-ons';
10
11
  import * as churchCenter from '@planningcenter/icons/paths/church-center';
@@ -940,8 +941,8 @@ const AppsProvider = ({
940
941
  const notificationsEnabled = !window.notificationsDisabled;
941
942
  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";
942
943
  const algoliaSearchAuth = {
943
- 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,
944
- 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
944
+ 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,
945
+ 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
945
946
  };
946
947
  const joltPromise = useMemo(() => {
947
948
  return resolveJoltPromise({
@@ -2224,10 +2225,750 @@ const sortByAttribute = attr => arr => arr.slice().sort((a, b) => a[attr].locale
2224
2225
  const appsMenuFormatter = apps => sortByAttribute("name")(apps).reduce((acc, item) => (item.name || "") === "Church Center" || (item.name || "") === "API" ? acc : item.name === "Home" ? [item, ...acc] : [...acc, item], []);
2225
2226
  const connectedPeopleMenuFormatter = connectedPeople => connectedPeople.slice().sort((a, b) => a.attributes["organization_name"].localeCompare(b.attributes["organization_name"]));
2226
2227
 
2228
+ // Generated by scripts/write-version.cjs from package.json. Do not edit.
2229
+ const BRIDGE_VERSION = "0.0.7-qa-41.0";
2230
+
2231
+ /**
2232
+ * PCA_BRIDGE_BUS_INIT — the script injected via
2233
+ * `injectedJavaScriptBeforeContentLoaded` before any web content loads.
2234
+ *
2235
+ * Installs `window.__pcaBridge` in the WebView's JS context. The bus:
2236
+ * - is per-WebView-context-by-construction (each WebView has its own JS
2237
+ * context; the global lives only in that context)
2238
+ * - accepts multiple registered handlers per reserved message type so
2239
+ * multiple Bridge instances in the same WebView can coexist
2240
+ * - buffers messages of a type with no registered handlers for up to 5s
2241
+ * - pings native with `__bridge/busReady` after install so the native
2242
+ * transport can flush its outgoing queue and trigger `reannounceLocal()`
2243
+ *
2244
+ * The bus runs as an IIFE so consumers can dispatch and register against it
2245
+ * immediately. Internal state (buffers, handlers) is closure-private.
2246
+ */
2247
+
2248
+ const PCA_BRIDGE_BUS_INIT = `
2249
+ (function () {
2250
+ if (typeof window === 'undefined') return
2251
+ if (window.__pcaBridge) return // idempotent in case it's injected twice
2252
+
2253
+ var BRIDGE_VERSION = ${JSON.stringify(BRIDGE_VERSION)}
2254
+ var BUFFER_TTL_MS = 5000
2255
+ var handlers = Object.create(null) // type -> Array<fn>
2256
+ var buffer = Object.create(null) // type -> Array<{msg, receivedAt}>
2257
+ var logger = {
2258
+ debug: function () {},
2259
+ warn: function (msg, ctx) {
2260
+ try { console.warn('[pca-bridge]', msg, ctx) } catch (e) {}
2261
+ },
2262
+ error: function (msg, ctx) {
2263
+ try { console.error('[pca-bridge]', msg, ctx) } catch (e) {}
2264
+ }
2265
+ }
2266
+
2267
+ function now() { return Date.now() }
2268
+
2269
+ function sweepBuffer() {
2270
+ var cutoff = now() - BUFFER_TTL_MS
2271
+ var dropped = []
2272
+ var hasAny = false
2273
+ for (var type in buffer) {
2274
+ var entries = buffer[type]
2275
+ var kept = []
2276
+ for (var i = 0; i < entries.length; i++) {
2277
+ if (entries[i].receivedAt >= cutoff) {
2278
+ kept.push(entries[i])
2279
+ } else {
2280
+ dropped.push({ type: type, ageMs: now() - entries[i].receivedAt })
2281
+ }
2282
+ }
2283
+ if (kept.length === 0) delete buffer[type]
2284
+ else {
2285
+ buffer[type] = kept
2286
+ hasAny = true
2287
+ }
2288
+ }
2289
+ if (dropped.length > 0 && window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
2290
+ try {
2291
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2292
+ type: '__bridge/drop',
2293
+ bridgeVersion: BRIDGE_VERSION,
2294
+ messages: dropped
2295
+ }))
2296
+ } catch (e) {}
2297
+ }
2298
+ // Stop ticking when the buffer drains; ensureSweep() restarts on demand.
2299
+ if (!hasAny && sweepInterval !== null) {
2300
+ clearInterval(sweepInterval)
2301
+ sweepInterval = null
2302
+ }
2303
+ }
2304
+
2305
+ var sweepInterval = null
2306
+ function ensureSweep() {
2307
+ if (sweepInterval !== null) return
2308
+ sweepInterval = setInterval(sweepBuffer, 1000)
2309
+ }
2310
+
2311
+ function register(type, fn) {
2312
+ if (!handlers[type]) handlers[type] = []
2313
+ handlers[type].push(fn)
2314
+ // Drain any buffered messages for this type, applying the same TTL the
2315
+ // background sweep uses so entries that already expired don't get
2316
+ // delivered just because sweep hasn't run yet.
2317
+ var pending = buffer[type]
2318
+ if (pending && pending.length > 0) {
2319
+ delete buffer[type]
2320
+ var cutoff = now() - BUFFER_TTL_MS
2321
+ for (var i = 0; i < pending.length; i++) {
2322
+ if (pending[i].receivedAt < cutoff) continue
2323
+ try { fn(pending[i].msg) } catch (e) {
2324
+ logger.error('Handler threw while draining buffer', { type: type, error: e })
2325
+ }
2326
+ }
2327
+ }
2328
+ }
2329
+
2330
+ function unregister(type, fn) {
2331
+ var arr = handlers[type]
2332
+ if (!arr) return
2333
+ var idx = arr.indexOf(fn)
2334
+ if (idx >= 0) arr.splice(idx, 1)
2335
+ if (arr.length === 0) delete handlers[type]
2336
+ }
2337
+
2338
+ function dispatch(msg) {
2339
+ if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return
2340
+ var type = msg.type
2341
+ var arr = handlers[type]
2342
+ if (arr && arr.length > 0) {
2343
+ // Snapshot the array to avoid issues if a handler registers/unregisters during dispatch
2344
+ var snapshot = arr.slice()
2345
+ for (var i = 0; i < snapshot.length; i++) {
2346
+ try { snapshot[i](msg) } catch (e) {
2347
+ logger.error('Handler threw during dispatch', { type: type, error: e })
2348
+ }
2349
+ }
2350
+ } else {
2351
+ if (!buffer[type]) buffer[type] = []
2352
+ buffer[type].push({ msg: msg, receivedAt: now() })
2353
+ ensureSweep()
2354
+ }
2355
+ }
2356
+
2357
+ function setLogger(next) {
2358
+ // Validate both methods the bus actually calls; a partial logger would
2359
+ // throw TypeError when dispatch/register tries to invoke a missing one.
2360
+ if (next && typeof next.warn === 'function' && typeof next.error === 'function') {
2361
+ logger = next
2362
+ }
2363
+ }
2364
+
2365
+ window.__pcaBridge = {
2366
+ register: register,
2367
+ unregister: unregister,
2368
+ dispatch: dispatch,
2369
+ setLogger: setLogger
2370
+ }
2371
+
2372
+ // Ping native: the bus is ready in this JS context. Native transport
2373
+ // listens for this to (a) flush its outgoing queue and (b) call
2374
+ // bridge.reannounceLocal() so the freshly-loaded web context learns about
2375
+ // already-exposed local actions/values.
2376
+ try {
2377
+ if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
2378
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: '__bridge/busReady', bridgeVersion: BRIDGE_VERSION }))
2379
+ }
2380
+ } catch (e) {}
2381
+ })();
2382
+ true;
2383
+ `;
2384
+
2385
+ class TimeoutError extends Error {
2386
+ constructor(name, timeoutMs) {
2387
+ super(`Call to "${name}" timed out after ${timeoutMs}ms`);
2388
+ this.name = 'TimeoutError';
2389
+ }
2390
+ }
2391
+ class HandlerError extends Error {
2392
+ constructor(name, code, originalMessage) {
2393
+ super(`Handler for "${name}" threw [${code}]: ${originalMessage}`);
2394
+ this.name = 'HandlerError';
2395
+ }
2396
+ }
2397
+ class DisposedError extends Error {
2398
+ constructor(name) {
2399
+ super(`Bridge disposed while call to "${name}" was in flight`);
2400
+ this.name = 'DisposedError';
2401
+ }
2402
+ }
2403
+
2404
+ let idCounter = 0;
2405
+ function generateId() {
2406
+ idCounter += 1;
2407
+ return `pca-bridge-${idCounter}`;
2408
+ }
2409
+ const noopLogger = {
2410
+ debug: () => {},
2411
+ warn: () => {},
2412
+ error: () => {}
2413
+ };
2414
+ function isIndexable(x) {
2415
+ return typeof x === 'object' && x !== null;
2416
+ }
2417
+
2418
+ /**
2419
+ * Shallow equality, one level deep. Used by `BridgeCore.publish` to dedup
2420
+ * re-emissions of new-but-content-equal values (e.g., a freshly-built
2421
+ * `modalButtons` array whose contents didn't change between renders).
2422
+ */
2423
+ function shallowEqual(a, b) {
2424
+ const sameRef = Object.is(a, b);
2425
+ const bothComparableObjects = isIndexable(a) && isIndexable(b) && Array.isArray(a) === Array.isArray(b);
2426
+ let result;
2427
+ if (sameRef) {
2428
+ result = true;
2429
+ } else if (!bothComparableObjects) {
2430
+ result = false;
2431
+ } else {
2432
+ const keys = Object.keys(a);
2433
+ result = keys.length === Object.keys(b).length && keys.every(k => Object.prototype.hasOwnProperty.call(b, k) && Object.is(a[k], b[k]));
2434
+ }
2435
+ return result;
2436
+ }
2437
+ function isWireEnvelope(parsed) {
2438
+ return typeof parsed === 'object' && parsed !== null && 'type' in parsed && typeof parsed.type === 'string' && parsed.type.startsWith('__bridge/') && 'bridgeVersion' in parsed && typeof parsed.bridgeVersion === 'string';
2439
+ }
2440
+ function parseWireEnvelope(rawData) {
2441
+ let parsed;
2442
+ try {
2443
+ parsed = JSON.parse(rawData);
2444
+ } catch {
2445
+ parsed = null;
2446
+ }
2447
+ return isWireEnvelope(parsed) ? parsed : null;
2448
+ }
2449
+
2450
+ const DEFAULT_TIMEOUT_MS = 5000;
2451
+ const INCOMING_CALL_QUEUE_TTL_MS = 5000;
2452
+ const INCOMING_CALL_QUEUE_MAX_PER_ACTION = 32;
2453
+ class BridgeCore {
2454
+ injectedJavaScriptBeforeContentLoaded = PCA_BRIDGE_BUS_INIT;
2455
+ localVersion = BRIDGE_VERSION;
2456
+ // `never` arg type lets us widen any user-defined `LocalActions[K]` callable
2457
+ // with a single `as` cast inside `expose()`.
2458
+ localActions = new Map();
2459
+ localValues = new Map();
2460
+ remoteActions = new Set();
2461
+ remoteValues = new Map();
2462
+ actionListeners = new Map();
2463
+ valueListeners = new Map();
2464
+ pendingCalls = new Map();
2465
+ callQueue = new Map();
2466
+
2467
+ // Private backing field + public getter so the `readonly` declared on the
2468
+ // Bridge interface is actually enforced — not just at the interface level
2469
+ // but for any code holding a concrete BridgeCore reference.
2470
+ _remoteVersion = undefined;
2471
+ get remoteVersion() {
2472
+ return this._remoteVersion;
2473
+ }
2474
+ disposed = false;
2475
+ constructor(options) {
2476
+ const sendRaw = options.send;
2477
+ this.send = msg => {
2478
+ sendRaw({
2479
+ ...msg,
2480
+ bridgeVersion: BRIDGE_VERSION
2481
+ });
2482
+ };
2483
+ this.logger = options.logger ?? noopLogger;
2484
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
2485
+ this.now = options.now ?? Date.now;
2486
+ }
2487
+ call(name, args, options) {
2488
+ const requestId = generateId();
2489
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
2490
+ return new Promise((resolve, reject) => {
2491
+ const timer = setTimeout(() => {
2492
+ if (this.pendingCalls.delete(requestId)) {
2493
+ reject(new TimeoutError(name, timeoutMs));
2494
+ }
2495
+ }, timeoutMs);
2496
+ // Heterogeneous map: cast the typed resolve to the loose storage slot.
2497
+ this.pendingCalls.set(requestId, {
2498
+ name,
2499
+ resolve: resolve,
2500
+ reject,
2501
+ timer
2502
+ });
2503
+ this.send({
2504
+ type: '__bridge/call',
2505
+ requestId,
2506
+ name,
2507
+ args
2508
+ });
2509
+ });
2510
+ }
2511
+ expose(name, handler) {
2512
+ if (this.localActions.has(name)) {
2513
+ this.logger.warn('Action already exposed; last-write-wins (the previous handler is gone)', {
2514
+ name
2515
+ });
2516
+ }
2517
+ this.localActions.set(name, handler);
2518
+ this.send({
2519
+ type: '__bridge/exposed',
2520
+ name,
2521
+ kind: 'action'
2522
+ });
2523
+ const queued = this.callQueue.get(name);
2524
+ if (queued) {
2525
+ this.callQueue.delete(name);
2526
+ const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
2527
+ for (const entry of queued) {
2528
+ if (entry.receivedAt >= cutoff) {
2529
+ void this.invokeLocalHandler(entry.msg);
2530
+ } else {
2531
+ this.logger.warn('Queued incoming call expired before handler registered', {
2532
+ name,
2533
+ ageMs: this.now() - entry.receivedAt
2534
+ });
2535
+ this.rejectQueued(entry, 'expired');
2536
+ }
2537
+ }
2538
+ }
2539
+ }
2540
+ unexpose(name) {
2541
+ if (this.localActions.delete(name)) {
2542
+ this.send({
2543
+ type: '__bridge/unexposed',
2544
+ name,
2545
+ kind: 'action'
2546
+ });
2547
+ }
2548
+ }
2549
+ hasRemoteAction(name) {
2550
+ return this.remoteActions.has(name);
2551
+ }
2552
+ onRemoteAction(name, callback) {
2553
+ let set = this.actionListeners.get(name);
2554
+ if (!set) {
2555
+ set = new Set();
2556
+ this.actionListeners.set(name, set);
2557
+ }
2558
+ set.add(callback);
2559
+ return () => {
2560
+ const current = this.actionListeners.get(name);
2561
+ if (current) {
2562
+ current.delete(callback);
2563
+ if (current.size === 0) {
2564
+ this.actionListeners.delete(name);
2565
+ }
2566
+ }
2567
+ };
2568
+ }
2569
+ publish(name, value) {
2570
+ // Skip wire send if content unchanged — callers may publish on every
2571
+ // render with a fresh-but-content-equal value.
2572
+ if (!shallowEqual(this.localValues.get(name), value)) {
2573
+ this.localValues.set(name, value);
2574
+ this.send({
2575
+ type: '__bridge/publish',
2576
+ name,
2577
+ value
2578
+ });
2579
+ }
2580
+ }
2581
+ unpublish(name) {
2582
+ if (this.localValues.delete(name)) {
2583
+ this.send({
2584
+ type: '__bridge/unexposed',
2585
+ name,
2586
+ kind: 'value'
2587
+ });
2588
+ }
2589
+ }
2590
+ seedLocalValue(name, value) {
2591
+ this.localValues.set(name, value);
2592
+ }
2593
+ getRemoteValue(name) {
2594
+ return this.remoteValues.get(name);
2595
+ }
2596
+ onRemoteValue(name, callback) {
2597
+ let set = this.valueListeners.get(name);
2598
+ if (!set) {
2599
+ set = new Set();
2600
+ this.valueListeners.set(name, set);
2601
+ }
2602
+ const wrapped = callback;
2603
+ set.add(wrapped);
2604
+ return () => {
2605
+ const current = this.valueListeners.get(name);
2606
+ if (current) {
2607
+ current.delete(wrapped);
2608
+ if (current.size === 0) {
2609
+ this.valueListeners.delete(name);
2610
+ }
2611
+ }
2612
+ };
2613
+ }
2614
+ reannounceLocal() {
2615
+ for (const name of this.localActions.keys()) {
2616
+ this.send({
2617
+ type: '__bridge/exposed',
2618
+ name,
2619
+ kind: 'action'
2620
+ });
2621
+ }
2622
+ for (const [name, value] of this.localValues) {
2623
+ this.send({
2624
+ type: '__bridge/exposed',
2625
+ name,
2626
+ kind: 'value',
2627
+ currentValue: value
2628
+ });
2629
+ }
2630
+ }
2631
+
2632
+ /**
2633
+ * Drop remote-side cached state. Called by transports when the remote JS
2634
+ * context is about to be torn down (e.g., WebView reload): the new context
2635
+ * will re-announce what it actually exposes, and stale entries would
2636
+ * otherwise silently mask removed actions/values.
2637
+ */
2638
+ clearRemoteState() {
2639
+ for (const name of this.remoteActions) {
2640
+ this.notifySet(this.actionListeners, name, false, 'action availability');
2641
+ }
2642
+ this.remoteActions.clear();
2643
+ for (const name of this.remoteValues.keys()) {
2644
+ this.notifySet(this.valueListeners, name, undefined, 'remote value change');
2645
+ }
2646
+ this.remoteValues.clear();
2647
+ this._remoteVersion = undefined;
2648
+ }
2649
+ handleMessage(rawData) {
2650
+ const msg = parseWireEnvelope(rawData);
2651
+ let consumed = false;
2652
+ if (msg) {
2653
+ this.handleIncomingMessage(msg);
2654
+ consumed = true;
2655
+ }
2656
+ return consumed;
2657
+ }
2658
+ applyLogger(logger) {
2659
+ if (this.logger === noopLogger) {
2660
+ this.logger = logger;
2661
+ return true;
2662
+ }
2663
+ return false;
2664
+ }
2665
+ handleLoadStart() {}
2666
+ handleLoadEnd() {}
2667
+ handleNavigationStateChange(navState) {
2668
+ if (!navState.loading) {
2669
+ this.handleLoadEnd();
2670
+ }
2671
+ }
2672
+ handleIncomingMessage(msg) {
2673
+ if (!this.disposed) {
2674
+ this._remoteVersion = msg.bridgeVersion;
2675
+ switch (msg.type) {
2676
+ case '__bridge/call':
2677
+ void this.routeIncomingCall(msg);
2678
+ break;
2679
+ case '__bridge/response':
2680
+ this.handleIncomingResponse(msg);
2681
+ break;
2682
+ case '__bridge/exposed':
2683
+ this.handleIncomingExposed(msg);
2684
+ break;
2685
+ case '__bridge/unexposed':
2686
+ this.handleIncomingUnexposed(msg);
2687
+ break;
2688
+ case '__bridge/publish':
2689
+ this.handleIncomingPublish(msg);
2690
+ break;
2691
+ case '__bridge/busReady':
2692
+ case '__bridge/drop':
2693
+ break;
2694
+ default:
2695
+ {
2696
+ // parseWireEnvelope admits any `__bridge/*` type; this catches
2697
+ // future message types a remote on a newer bridge sends to us.
2698
+ const unknown = msg;
2699
+ this.logger.warn('Received unknown bridge message type; ignoring', {
2700
+ type: unknown.type,
2701
+ remoteVersion: this.remoteVersion
2702
+ });
2703
+ break;
2704
+ }
2705
+ }
2706
+ }
2707
+ }
2708
+ get debug() {
2709
+ return {
2710
+ listLocalActions: () => Array.from(this.localActions.keys()),
2711
+ listRemoteActions: () => Array.from(this.remoteActions),
2712
+ listLocalValues: () => Array.from(this.localValues.keys()),
2713
+ listRemoteValues: () => Array.from(this.remoteValues.keys())
2714
+ };
2715
+ }
2716
+ dispose() {
2717
+ if (!this.disposed) {
2718
+ this.disposed = true;
2719
+ for (const pending of this.pendingCalls.values()) {
2720
+ clearTimeout(pending.timer);
2721
+ pending.reject(new DisposedError(pending.name));
2722
+ }
2723
+ this.pendingCalls.clear();
2724
+ this.callQueue.clear();
2725
+ }
2726
+ }
2727
+ async routeIncomingCall(msg) {
2728
+ if (this.localActions.has(msg.name)) {
2729
+ await this.invokeLocalHandler(msg);
2730
+ return;
2731
+ }
2732
+ let queued = this.callQueue.get(msg.name);
2733
+ if (!queued) {
2734
+ queued = [];
2735
+ this.callQueue.set(msg.name, queued);
2736
+ }
2737
+ // Evict TTL-expired and over-capacity entries so a remote that spams an
2738
+ // unknown action can't grow the queue without bound. Each evicted call
2739
+ // gets an error response so the caller fails fast.
2740
+ const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
2741
+ while (queued.length > 0) {
2742
+ const head = queued[0];
2743
+ if (!head || head.receivedAt >= cutoff) break;
2744
+ queued.shift();
2745
+ this.rejectQueued(head, 'expired');
2746
+ }
2747
+ while (queued.length >= INCOMING_CALL_QUEUE_MAX_PER_ACTION) {
2748
+ const evicted = queued.shift();
2749
+ if (!evicted) break;
2750
+ this.rejectQueued(evicted, 'queue_full');
2751
+ }
2752
+ queued.push({
2753
+ msg,
2754
+ receivedAt: this.now()
2755
+ });
2756
+ }
2757
+ rejectQueued(entry, code) {
2758
+ this.send({
2759
+ type: '__bridge/response',
2760
+ requestId: entry.msg.requestId,
2761
+ ok: false,
2762
+ code,
2763
+ message: `Queued call to "${entry.msg.name}" dropped (${code})`
2764
+ });
2765
+ }
2766
+ async invokeLocalHandler(msg) {
2767
+ // Handler may have been unregistered between queue push and drain. Reply
2768
+ // with an explicit error so the caller fails fast instead of timing out.
2769
+ const handler = this.localActions.get(msg.name);
2770
+ if (!handler) {
2771
+ this.send({
2772
+ type: '__bridge/response',
2773
+ requestId: msg.requestId,
2774
+ ok: false,
2775
+ code: 'unexposed',
2776
+ message: `No handler exposed for "${msg.name}"`
2777
+ });
2778
+ return;
2779
+ }
2780
+ try {
2781
+ const result = await handler(msg.args);
2782
+ this.send({
2783
+ type: '__bridge/response',
2784
+ requestId: msg.requestId,
2785
+ ok: true,
2786
+ result
2787
+ });
2788
+ } catch (error) {
2789
+ this.send({
2790
+ type: '__bridge/response',
2791
+ requestId: msg.requestId,
2792
+ ok: false,
2793
+ code: 'thrown',
2794
+ message: error instanceof Error ? error.message : String(error)
2795
+ });
2796
+ }
2797
+ }
2798
+ handleIncomingResponse(msg) {
2799
+ const pending = this.pendingCalls.get(msg.requestId);
2800
+ if (pending) {
2801
+ this.pendingCalls.delete(msg.requestId);
2802
+ clearTimeout(pending.timer);
2803
+ if (msg.ok) {
2804
+ pending.resolve(msg.result);
2805
+ } else {
2806
+ pending.reject(new HandlerError(pending.name, msg.code, msg.message));
2807
+ }
2808
+ } else {
2809
+ this.logger.warn('Response for unknown or already-resolved requestId', {
2810
+ requestId: msg.requestId
2811
+ });
2812
+ }
2813
+ }
2814
+ handleIncomingExposed(msg) {
2815
+ if (msg.kind === 'action') {
2816
+ if (!this.remoteActions.has(msg.name)) {
2817
+ this.remoteActions.add(msg.name);
2818
+ this.notifySet(this.actionListeners, msg.name, true, 'action availability');
2819
+ }
2820
+ } else {
2821
+ this.remoteValues.set(msg.name, msg.currentValue);
2822
+ this.notifySet(this.valueListeners, msg.name, msg.currentValue, 'remote value change');
2823
+ }
2824
+ }
2825
+ handleIncomingUnexposed(msg) {
2826
+ if (msg.kind === 'action') {
2827
+ if (this.remoteActions.delete(msg.name)) {
2828
+ this.notifySet(this.actionListeners, msg.name, false, 'action availability');
2829
+ }
2830
+ } else if (this.remoteValues.delete(msg.name)) {
2831
+ this.notifySet(this.valueListeners, msg.name, undefined, 'remote value change');
2832
+ }
2833
+ }
2834
+ handleIncomingPublish(msg) {
2835
+ this.remoteValues.set(msg.name, msg.value);
2836
+ this.notifySet(this.valueListeners, msg.name, msg.value, 'remote value change');
2837
+ }
2838
+ notifySet(subs, name, value, label) {
2839
+ const set = subs.get(name);
2840
+ if (set) {
2841
+ for (const callback of set) {
2842
+ try {
2843
+ callback(value);
2844
+ } catch (error) {
2845
+ this.logger.error(`Subscriber threw during ${label} notification`, {
2846
+ name,
2847
+ error
2848
+ });
2849
+ }
2850
+ }
2851
+ }
2852
+ }
2853
+ }
2854
+
2855
+ // Subset of window.__pcaBridge that bridge_web.ts needs.
2856
+
2857
+ const INBOUND_TYPES = ['__bridge/call', '__bridge/response', '__bridge/exposed', '__bridge/unexposed', '__bridge/publish'];
2858
+ class WebBridge extends BridgeCore {
2859
+ constructor(options = {}) {
2860
+ const bus = window.__pcaBridge;
2861
+ super({
2862
+ send: msg => {
2863
+ if (window.ReactNativeWebView) {
2864
+ try {
2865
+ window.ReactNativeWebView.postMessage(JSON.stringify(msg));
2866
+ } catch (error) {
2867
+ options.logger?.error('WebBridge postMessage failed', {
2868
+ error
2869
+ });
2870
+ }
2871
+ }
2872
+ },
2873
+ logger: options.logger,
2874
+ defaultTimeoutMs: options.defaultTimeoutMs
2875
+ });
2876
+ this.bus = bus;
2877
+ this.handlerFn = msg => {
2878
+ this.handleIncomingMessage(msg);
2879
+ };
2880
+ if (bus) {
2881
+ if (options.logger) {
2882
+ bus.setLogger(options.logger);
2883
+ }
2884
+ for (const type of INBOUND_TYPES) {
2885
+ bus.register(type, this.handlerFn);
2886
+ }
2887
+ if (window.ReactNativeWebView) {
2888
+ try {
2889
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2890
+ type: '__bridge/busReady',
2891
+ bridgeVersion: BRIDGE_VERSION
2892
+ }));
2893
+ } catch (error) {
2894
+ options.logger?.error('WebBridge busReady post failed', {
2895
+ error
2896
+ });
2897
+ }
2898
+ }
2899
+ } else {
2900
+ options.logger?.warn('WebBridge constructed without window.__pcaBridge — bus init script may not have run');
2901
+ }
2902
+ }
2903
+ applyLogger(logger) {
2904
+ const applied = super.applyLogger(logger);
2905
+ if (applied && this.bus) {
2906
+ this.bus.setLogger(logger);
2907
+ }
2908
+ return applied;
2909
+ }
2910
+ dispose() {
2911
+ if (this.bus) {
2912
+ for (const type of INBOUND_TYPES) {
2913
+ this.bus.unregister(type, this.handlerFn);
2914
+ }
2915
+ }
2916
+ super.dispose();
2917
+ }
2918
+ }
2919
+ function getOrCreateWebBridge(options = {}) {
2920
+ if (window.__pcaWebBridge && options.logger) {
2921
+ window.__pcaWebBridge.applyLogger(options.logger);
2922
+ } else if (!window.__pcaWebBridge) {
2923
+ window.__pcaWebBridge = new WebBridge(options);
2924
+ }
2925
+ return window.__pcaWebBridge;
2926
+ }
2927
+
2928
+ const WebBridgeContext = /*#__PURE__*/createContext(null);
2929
+ function useWebBridge() {
2930
+ const ctxBridge = useContext(WebBridgeContext);
2931
+ if (ctxBridge) {
2932
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
2933
+ return ctxBridge;
2934
+ } else {
2935
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
2936
+ return getOrCreateWebBridge();
2937
+ }
2938
+ }
2939
+ function useBridgePublish(bridge, name, value) {
2940
+ // publish() dedups by shallow equality, so this effect is safe to re-run
2941
+ // on every value change without spamming wire messages.
2942
+ useEffect(() => {
2943
+ bridge.publish(name, value);
2944
+ }, [bridge, name, value]);
2945
+ useEffect(() => {
2946
+ return () => {
2947
+ bridge.unpublish(name);
2948
+ };
2949
+ }, [bridge, name]);
2950
+ }
2951
+
2227
2952
  const useSetCurrentRouteInfo = ({
2228
2953
  routes,
2229
2954
  app
2230
2955
  }) => {
2956
+ const bridge = useWebBridge();
2957
+ useBridgePublish(bridge, "routeInfo", {
2958
+ currentApp: app,
2959
+ currentRoutes: routes.map(({
2960
+ key,
2961
+ props: {
2962
+ to,
2963
+ href,
2964
+ active
2965
+ }
2966
+ }) => ({
2967
+ active,
2968
+ name: key,
2969
+ url: to !== null && to !== void 0 ? to : href
2970
+ }))
2971
+ });
2231
2972
  useEffect(() => {
2232
2973
  if (window.topbar) {
2233
2974
  window.topbar.routeInfo = {
@@ -2247,6 +2988,24 @@ const useSetCurrentRouteInfo = ({
2247
2988
  };
2248
2989
  }
2249
2990
  }, [routes, app]);
2991
+ // Publish info for when app goes back
2992
+ useEffect(() => window.addEventListener("pageshow", () => {
2993
+ bridge.publish("routeInfo", {
2994
+ currentApp: app,
2995
+ currentRoutes: routes.map(({
2996
+ key,
2997
+ props: {
2998
+ to,
2999
+ href,
3000
+ active
3001
+ }
3002
+ }) => ({
3003
+ active,
3004
+ name: key,
3005
+ url: to !== null && to !== void 0 ? to : href
3006
+ }))
3007
+ });
3008
+ }), []);
2250
3009
  };
2251
3010
 
2252
3011
  const useUpdateStore = () => useCallback(updates => {
@@ -3454,40 +4213,44 @@ function buildPicobotSrc(origin, path) {
3454
4213
  return fragment ? `${base}#${fragment}` : base;
3455
4214
  }
3456
4215
 
3457
- const handleSearchSelect = (hit, indexName) => {
3458
- const appName = indexName.split("_")[1];
3459
- const zendeskUrl = `https://${appName}.zendesk.com/hc/${hit.locale.locale}/articles/${hit.id}`;
3460
- const newTab = window.open(zendeskUrl, "_blank");
4216
+ // The article heading lives in the deepest populated hierarchy level (lvl0 is
4217
+ // always "Planning Center").
4218
+ 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) || "";
4219
+ const handleSearchSelect = hit => {
4220
+ // `url` is always absolute (it is also the basis of objectID), so open it
4221
+ // directly. Bail out defensively if a record somehow arrives without one.
4222
+ if (!(hit === null || hit === void 0 ? void 0 : hit.url)) return;
4223
+ const newTab = window.open(hit.url, "_blank");
3461
4224
  if (newTab) {
3462
4225
  newTab.focus();
3463
4226
  }
3464
4227
  };
4228
+ // Mirrors @planningcenter/tapestry-react's Logo registry.
4229
+ 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"]);
3465
4230
  const HeadingIcon = ({
3466
- sectionName,
3467
- currentAppName
4231
+ currentAppName,
4232
+ product
3468
4233
  }) => {
3469
- const lowerCaseSectionName = sectionName.toLowerCase();
3470
- if (lowerCaseSectionName === "account information" || lowerCaseSectionName === "recent updates") {
3471
- return /*#__PURE__*/React__default.createElement(Icon, {
3472
- name: lowerCaseSectionName === "account information" ? "general.cogPerson" : "general.outlinedGenericFile",
3473
- color: "foregroundSecondary"
3474
- });
3475
- } else if (currentAppName === "Accounts") {
4234
+ const appName = product || currentAppName;
4235
+ if (!appName) return null;
4236
+ const logoName = appName.toLowerCase().replace(/\s+/g, "-");
4237
+ // Accounts has no tapestry Logo, so use the cog icon
4238
+ if (logoName === "accounts") {
3476
4239
  return /*#__PURE__*/React__default.createElement(Icon, {
3477
4240
  name: "general.cog",
3478
4241
  color: "foregroundSecondary"
3479
4242
  });
3480
- } else {
3481
- return /*#__PURE__*/React__default.createElement(StackView$2, {
3482
- width: 2,
3483
- height: 2
3484
- }, /*#__PURE__*/React__default.createElement(Logo, {
3485
- name: currentAppName.toLowerCase().replace(/[\s]/, "-"),
3486
- size: "sm",
3487
- theme: "color",
3488
- markOnly: true
3489
- }));
3490
4243
  }
4244
+ if (!VALID_LOGO_NAMES.has(logoName)) return null;
4245
+ return /*#__PURE__*/React__default.createElement(StackView$2, {
4246
+ width: 2,
4247
+ height: 2
4248
+ }, /*#__PURE__*/React__default.createElement(Logo, {
4249
+ name: logoName,
4250
+ size: "sm",
4251
+ theme: "color",
4252
+ markOnly: true
4253
+ }));
3491
4254
  };
3492
4255
  const HitResult = ({
3493
4256
  hit,
@@ -3502,11 +4265,10 @@ const HitResult = ({
3502
4265
  color: "neutralDarker",
3503
4266
  size: 4,
3504
4267
  whiteSpace: "normal"
3505
- }, hit.title));
4268
+ }, hitTitle(hit.hierarchy)));
3506
4269
  };
3507
4270
  const CustomSearchBox = ({
3508
- currentAppName,
3509
- indexName
4271
+ currentAppName
3510
4272
  }) => {
3511
4273
  const {
3512
4274
  query,
@@ -3517,7 +4279,10 @@ const CustomSearchBox = ({
3517
4279
  results
3518
4280
  } = useHits();
3519
4281
  const groups = results === null || results === void 0 ? void 0 : results.hits.reduce((acc, hit) => {
3520
- const key = hit.category.title;
4282
+ // A result with no url can't be opened, so don't render it at all.
4283
+ if (!hit.url) return acc;
4284
+ // Fall back so a record without a product never yields an "undefined" heading.
4285
+ const key = hit.product || currentAppName || "Other";
3521
4286
  if (!acc[key]) {
3522
4287
  acc[key] = [];
3523
4288
  }
@@ -3539,7 +4304,7 @@ const CustomSearchBox = ({
3539
4304
  name: "general.search"
3540
4305
  }),
3541
4306
  onChange: e => setQuery(e.target.value),
3542
- onSelect: selectedValue => handleSearchSelect(selectedValue, indexName),
4307
+ onSelect: selectedValue => handleSearchSelect(selectedValue),
3543
4308
  placement: "bottom-start"
3544
4309
  }, !hits.length && (/*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(StackView$2, {
3545
4310
  axis: "horizontal",
@@ -3551,7 +4316,7 @@ const CustomSearchBox = ({
3551
4316
  size: 4,
3552
4317
  whiteSpace: "normal"
3553
4318
  }, "No results match your search")))), groups && Object.keys(groups).map(key => {
3554
- const hits = groups[key];
4319
+ const groupHits = groups[key];
3555
4320
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, {
3556
4321
  key: key
3557
4322
  }, resultIndex > 0 && /*#__PURE__*/React__default.createElement(Divider, {
@@ -3564,12 +4329,12 @@ const CustomSearchBox = ({
3564
4329
  paddingVertical: 1
3565
4330
  }, /*#__PURE__*/React__default.createElement(HeadingIcon, {
3566
4331
  currentAppName: currentAppName,
3567
- sectionName: key
4332
+ product: key
3568
4333
  }), /*#__PURE__*/React__default.createElement(Text, {
3569
4334
  size: 5,
3570
4335
  color: "foregroundSecondary",
3571
4336
  fontWeight: 500
3572
- }, key)), hits.map(hit => (/*#__PURE__*/React__default.createElement(HitResult, {
4337
+ }, key)), groupHits.map(hit => (/*#__PURE__*/React__default.createElement(HitResult, {
3573
4338
  key: hit.objectID,
3574
4339
  hit: hit,
3575
4340
  index: resultIndex++
@@ -3577,8 +4342,8 @@ const CustomSearchBox = ({
3577
4342
  })));
3578
4343
  };
3579
4344
 
4345
+ const HELP_CENTER_INDEX = "help_center_english";
3580
4346
  const DocumentationSearch = ({
3581
- indexName,
3582
4347
  currentAppName
3583
4348
  }) => {
3584
4349
  const {
@@ -3589,13 +4354,13 @@ const DocumentationSearch = ({
3589
4354
  } = useStore();
3590
4355
  const searchClient = algoliasearch(algoliaApplicationId, algoliaSearchOnlyApiKey);
3591
4356
  return /*#__PURE__*/React__default.createElement(InstantSearch, {
3592
- indexName: indexName,
4357
+ indexName: HELP_CENTER_INDEX,
3593
4358
  searchClient: searchClient
3594
4359
  }, /*#__PURE__*/React__default.createElement(Configure, {
3595
- hitsPerPage: 5
4360
+ hitsPerPage: 5,
4361
+ optionalFilters: currentAppName ? [`product:${currentAppName}`] : undefined
3596
4362
  }), /*#__PURE__*/React__default.createElement(CustomSearchBox, {
3597
- currentAppName: currentAppName,
3598
- indexName: indexName
4363
+ currentAppName: currentAppName
3599
4364
  }));
3600
4365
  };
3601
4366
 
@@ -3603,6 +4368,19 @@ const {
3603
4368
  LoadingIndicator: LoadingIndicator$1,
3604
4369
  StackView: StackView$1
3605
4370
  } = components;
4371
+ function CalendlyWidget({
4372
+ url,
4373
+ pageSettings,
4374
+ prefill,
4375
+ styles
4376
+ }) {
4377
+ return url ? (/*#__PURE__*/React__default.createElement(InlineWidget, {
4378
+ url: url,
4379
+ pageSettings: pageSettings,
4380
+ prefill: prefill,
4381
+ styles: styles
4382
+ })) : null;
4383
+ }
3606
4384
  function HelpdeskComponent({
3607
4385
  appName,
3608
4386
  closeDrawer,
@@ -3671,14 +4449,65 @@ function HelpdeskComponent({
3671
4449
  spacing: 1
3672
4450
  }, /*#__PURE__*/React__default.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React__default.createElement(LoadingIndicator$1, null)))),
3673
4451
  additionalComponents: {
3674
- // @ts-expect-error TODO: Relax type specification in add-ons-js to not require children
3675
- CalendlyWidget: InlineWidget,
3676
- DocumentationSearch,
3677
- DrawerHeader,
4452
+ CalendlyWidget: {
4453
+ component: CalendlyWidget,
4454
+ properties: {
4455
+ pageSettings: {
4456
+ attribute: false
4457
+ },
4458
+ prefill: {
4459
+ attribute: false
4460
+ },
4461
+ styles: {
4462
+ attribute: false
4463
+ },
4464
+ url: {
4465
+ type: String
4466
+ }
4467
+ }
4468
+ },
4469
+ DocumentationSearch: {
4470
+ component: DocumentationSearch,
4471
+ properties: {
4472
+ currentAppName: {
4473
+ type: String
4474
+ }
4475
+ }
4476
+ },
4477
+ DrawerHeader: {
4478
+ component: DrawerHeader,
4479
+ properties: {
4480
+ closeDrawer: {
4481
+ event: false
4482
+ }
4483
+ }
4484
+ },
3678
4485
  DrawerHeading,
3679
- PicobotIframe,
4486
+ PicobotIframe: {
4487
+ component: PicobotIframe,
4488
+ properties: {
4489
+ currentPath: {
4490
+ attribute: false
4491
+ },
4492
+ onChangePath: {
4493
+ event: false,
4494
+ type: Function
4495
+ },
4496
+ onMessage: {
4497
+ event: false,
4498
+ type: Function
4499
+ }
4500
+ }
4501
+ },
3680
4502
  ScrollContainer,
3681
- SwitchView
4503
+ SwitchView: {
4504
+ component: SwitchView,
4505
+ properties: {
4506
+ activeIndex: {
4507
+ type: Number
4508
+ }
4509
+ }
4510
+ }
3682
4511
  },
3683
4512
  args: {
3684
4513
  appName,
@@ -4282,8 +5111,13 @@ const {
4282
5111
  function NavLinkWithBadge({
4283
5112
  children,
4284
5113
  badge,
4285
- onClick,
4286
- ...rest
5114
+ borderBottomColor,
5115
+ borderBottomStyle,
5116
+ borderBottomWidth,
5117
+ color,
5118
+ fontSize,
5119
+ fontWeight,
5120
+ onClick
4287
5121
  }) {
4288
5122
  return /*#__PURE__*/React__default.createElement(StackView$2, {
4289
5123
  as: "button",
@@ -4293,7 +5127,14 @@ function NavLinkWithBadge({
4293
5127
  paddingBottom: 0.75,
4294
5128
  onClick: () => onClick(),
4295
5129
  spacing: 1,
4296
- ...rest
5130
+ ...{
5131
+ borderBottomColor,
5132
+ borderBottomStyle,
5133
+ borderBottomWidth,
5134
+ color,
5135
+ fontSize,
5136
+ fontWeight
5137
+ }
4297
5138
  }, children, badge && /*#__PURE__*/React__default.createElement(Badge, {
4298
5139
  size: "sm",
4299
5140
  fontWeight: "600",
@@ -4345,11 +5186,55 @@ function TasksComponent({
4345
5186
  spacing: 1
4346
5187
  }, /*#__PURE__*/React__default.createElement(DrawerHeading, null, "To do"), /*#__PURE__*/React__default.createElement(LoadingIndicator, null)))),
4347
5188
  additionalComponents: {
4348
- DrawerHeader,
5189
+ DrawerHeader: {
5190
+ component: DrawerHeader,
5191
+ properties: {
5192
+ closeDrawer: {
5193
+ event: false,
5194
+ type: Function
5195
+ }
5196
+ }
5197
+ },
4349
5198
  DrawerHeading,
4350
- NavLinkWithBadge,
5199
+ NavLinkWithBadge: {
5200
+ component: NavLinkWithBadge,
5201
+ properties: {
5202
+ badge: {
5203
+ attribute: false
5204
+ },
5205
+ borderBottomColor: {
5206
+ type: String
5207
+ },
5208
+ borderBottomStyle: {
5209
+ type: String
5210
+ },
5211
+ borderBottomWidth: {
5212
+ type: String
5213
+ },
5214
+ color: {
5215
+ type: String
5216
+ },
5217
+ fontSize: {
5218
+ attribute: false
5219
+ },
5220
+ fontWeight: {
5221
+ attribute: false
5222
+ },
5223
+ onClick: {
5224
+ event: false,
5225
+ type: Function
5226
+ }
5227
+ }
5228
+ },
4351
5229
  ScrollContainer,
4352
- SwitchView
5230
+ SwitchView: {
5231
+ component: SwitchView,
5232
+ properties: {
5233
+ activeIndex: {
5234
+ type: Number
5235
+ }
5236
+ }
5237
+ }
4353
5238
  },
4354
5239
  args: {
4355
5240
  closeDrawer,