@bash-app/bash-common 30.367.0 → 30.370.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared shapes for the bash-ticket Live Activity (iOS ActivityKit Lock Screen /
3
+ * Dynamic Island) and Android Live Update / sticky-notification fallback.
4
+ *
5
+ * `api` builds `LiveActivityContentState` server-side to push via APNs
6
+ * (push-to-start/update/end) or an FCM data message; `bash-app` uses the same
7
+ * shape to type the native plugin's `start`/`update` calls for the manual
8
+ * "Track this bash" toggle. Keeping this in bash-common avoids the two repos
9
+ * drifting on field names for what is ultimately one wire contract.
10
+ */
11
+
12
+ /** Static, per-activity identifiers — set once, never change during the activity. */
13
+ export interface LiveActivityAttributes {
14
+ bashEventId: string;
15
+ ticketId: string;
16
+ }
17
+
18
+ /**
19
+ * Dynamic content shown on the Lock Screen / Dynamic Island / Android chip.
20
+ * Kept intentionally small — ActivityKit caps combined attributes + state at 4KB,
21
+ * and every field here is re-sent in full on every update (no partial merges).
22
+ */
23
+ export interface LiveActivityContentState {
24
+ /** Bash event title — primary headline. */
25
+ eventTitle: string;
26
+ /** Short venue/location line — subtitle (e.g. venue name or first line of address). */
27
+ venueLabel: string;
28
+ /** ISO 8601 — bash start time. Widget shows a countdown until this, then a "LIVE" badge. */
29
+ startTime: string;
30
+ /** ISO 8601 — bash end time. Used for the lazy client-side end fallback. */
31
+ endTime: string;
32
+ /**
33
+ * Absolute URL that opens the device's native Maps app for directions
34
+ * (Apple Maps on iOS, Google Maps on Android) — never a relative path,
35
+ * since the widget/notification has no app routing context.
36
+ */
37
+ directionsUrl: string;
38
+ /**
39
+ * Absolute universal-link URL back into bash-app's ticket QR screen
40
+ * (`bashEventQrCodeUrl` + the app's public origin). Deliberately NOT an
41
+ * embedded QR image/token — keeps the payload small and the QR always fresh.
42
+ */
43
+ ticketUrl: string;
44
+ }
45
+
46
+ /** Discriminates the three lifecycle pushes sent over APNs (iOS) or as an FCM data message (Android). */
47
+ export type LiveActivityPushEventType =
48
+ | "live_activity_start"
49
+ | "live_activity_update"
50
+ | "live_activity_end";
51
+
52
+ /** Platform values accepted on `LiveActivityPushToken.platform` — iOS only today (Android reuses the FCM token). */
53
+ export const LIVE_ACTIVITY_TOKEN_PLATFORM_IOS = "ios" as const;
54
+
55
+ /** Which native Maps app `buildLiveActivityDirectionsUrl` should target. */
56
+ export type LiveActivityMapsPlatform = "ios" | "android";
57
+
58
+ /**
59
+ * Builds the "Directions" deep link shared by the api (server-built push
60
+ * payloads) and bash-app (client-built manual-start payload) — Apple Maps on
61
+ * iOS, Google Maps everywhere else, preferring coordinates over the raw
62
+ * address/title string when available.
63
+ */
64
+ export function buildLiveActivityDirectionsUrl(
65
+ destinationCoords: { lat: number; lng: number } | null | undefined,
66
+ fallbackLabel: string,
67
+ platform: LiveActivityMapsPlatform
68
+ ): string {
69
+ const destination = destinationCoords
70
+ ? `${destinationCoords.lat},${destinationCoords.lng}`
71
+ : encodeURIComponent(fallbackLabel);
72
+ return platform === "ios"
73
+ ? `https://maps.apple.com/?daddr=${destination}`
74
+ : `https://www.google.com/maps/search/?api=1&query=${destination}`;
75
+ }
@@ -4,6 +4,7 @@ import {
4
4
  organizationHasApiAccess,
5
5
  organizationHasAuditLogAccess,
6
6
  organizationHasComplianceAccess,
7
+ organizationHasCustomDomainAccess,
7
8
  organizationHasEnterprisePlatformAccess,
8
9
  organizationHasProcurementAccess,
9
10
  organizationHasSsoAccess,
@@ -67,41 +68,79 @@ describe("organizationHasVanitySubdomain", () => {
67
68
  });
68
69
 
69
70
  describe("enterprise platform gates (audit logs, SSO, procurement, API access)", () => {
70
- const enterpriseActive = {
71
+ // Sales-provisioned: ops set enterpriseSalesProvisioned via the admin
72
+ // enterprise-provisioning endpoint after a signed deal.
73
+ const salesProvisionedEnterprise = {
71
74
  subscriptionTier: "Enterprise",
72
75
  subscriptionStatus: "Active",
76
+ enterpriseSalesProvisioned: true,
77
+ };
78
+ // Self-serve: bought via Stripe checkout, never grants the platform surface.
79
+ const selfServeEnterprise = {
80
+ subscriptionTier: "Enterprise",
81
+ subscriptionStatus: "Active",
82
+ enterpriseSalesProvisioned: false,
73
83
  };
74
84
  const growthActive = { subscriptionTier: "Growth", subscriptionStatus: "Active" };
75
85
 
76
- test("organizationHasEnterprisePlatformAccess mirrors organizationHasVanitySubdomain", () => {
77
- expect(organizationHasEnterprisePlatformAccess(enterpriseActive)).toBe(true);
86
+ test("organizationHasEnterprisePlatformAccess requires enterpriseSalesProvisioned", () => {
87
+ expect(organizationHasEnterprisePlatformAccess(salesProvisionedEnterprise)).toBe(true);
88
+ expect(organizationHasEnterprisePlatformAccess(selfServeEnterprise)).toBe(false);
78
89
  expect(organizationHasEnterprisePlatformAccess(growthActive)).toBe(false);
79
90
  expect(organizationHasEnterprisePlatformAccess(null)).toBe(false);
80
91
  expect(organizationHasEnterprisePlatformAccess(undefined)).toBe(false);
81
92
  });
82
93
 
94
+ test("missing enterpriseSalesProvisioned on an Enterprise org fails closed (not entitled)", () => {
95
+ expect(
96
+ organizationHasEnterprisePlatformAccess({
97
+ subscriptionTier: "Enterprise",
98
+ subscriptionStatus: "Active",
99
+ })
100
+ ).toBe(false);
101
+ });
102
+
83
103
  test("organizationHasAuditLogAccess follows the shared enterprise gate", () => {
84
- expect(organizationHasAuditLogAccess(enterpriseActive)).toBe(true);
104
+ expect(organizationHasAuditLogAccess(salesProvisionedEnterprise)).toBe(true);
105
+ expect(organizationHasAuditLogAccess(selfServeEnterprise)).toBe(false);
85
106
  expect(organizationHasAuditLogAccess(growthActive)).toBe(false);
86
107
  });
87
108
 
88
109
  test("organizationHasSsoAccess follows the shared enterprise gate", () => {
89
- expect(organizationHasSsoAccess(enterpriseActive)).toBe(true);
110
+ expect(organizationHasSsoAccess(salesProvisionedEnterprise)).toBe(true);
111
+ expect(organizationHasSsoAccess(selfServeEnterprise)).toBe(false);
90
112
  expect(organizationHasSsoAccess(growthActive)).toBe(false);
91
113
  });
92
114
 
93
115
  test("organizationHasProcurementAccess follows the shared enterprise gate", () => {
94
- expect(organizationHasProcurementAccess(enterpriseActive)).toBe(true);
116
+ expect(organizationHasProcurementAccess(salesProvisionedEnterprise)).toBe(true);
117
+ expect(organizationHasProcurementAccess(selfServeEnterprise)).toBe(false);
95
118
  expect(organizationHasProcurementAccess(growthActive)).toBe(false);
96
119
  });
97
120
 
98
121
  test("organizationHasApiAccess follows the shared enterprise gate", () => {
99
- expect(organizationHasApiAccess(enterpriseActive)).toBe(true);
122
+ expect(organizationHasApiAccess(salesProvisionedEnterprise)).toBe(true);
123
+ expect(organizationHasApiAccess(selfServeEnterprise)).toBe(false);
100
124
  expect(organizationHasApiAccess(growthActive)).toBe(false);
101
125
  });
102
126
 
103
127
  test("organizationHasComplianceAccess follows the shared enterprise gate", () => {
104
- expect(organizationHasComplianceAccess(enterpriseActive)).toBe(true);
128
+ expect(organizationHasComplianceAccess(salesProvisionedEnterprise)).toBe(true);
129
+ expect(organizationHasComplianceAccess(selfServeEnterprise)).toBe(false);
105
130
  expect(organizationHasComplianceAccess(growthActive)).toBe(false);
106
131
  });
132
+
133
+ test("organizationHasCustomDomainAccess (white-label) is self-serve-eligible — no sales flag required", () => {
134
+ expect(organizationHasCustomDomainAccess(salesProvisionedEnterprise)).toBe(true);
135
+ expect(organizationHasCustomDomainAccess(selfServeEnterprise)).toBe(true);
136
+ expect(organizationHasCustomDomainAccess(growthActive)).toBe(false);
137
+ expect(organizationHasCustomDomainAccess(null)).toBe(false);
138
+ expect(organizationHasCustomDomainAccess(undefined)).toBe(false);
139
+ });
140
+
141
+ test("organizationHasVanitySubdomain is self-serve-eligible — no sales flag required", () => {
142
+ expect(organizationHasVanitySubdomain(salesProvisionedEnterprise)).toBe(true);
143
+ expect(organizationHasVanitySubdomain(selfServeEnterprise)).toBe(true);
144
+ expect(organizationHasVanitySubdomain(growthActive)).toBe(false);
145
+ });
107
146
  });
@@ -15,14 +15,17 @@ export const ACTIVE_ORG_SUBSCRIPTION_STATUSES = [
15
15
  export interface OrganizationEntitlementInput {
16
16
  subscriptionTier?: string | null;
17
17
  subscriptionStatus?: string | null;
18
+ /**
19
+ * True only when ops provisioned Enterprise via a signed deal (admin
20
+ * enterprise-provisioning endpoint). Self-serve Stripe checkout can also set
21
+ * subscriptionTier="Enterprise" but never sets this. Optional/undefined is
22
+ * treated as false so legacy call sites that don't select this field fail
23
+ * closed on the sales-gated surface rather than open.
24
+ */
25
+ enterpriseSalesProvisioned?: boolean | null;
18
26
  }
19
27
 
20
- /**
21
- * Vanity subdomains ({slug}.bash.community) are an Enterprise-only perk. All other
22
- * orgs use the canonical path URL (bash.community/org/{slug}). Requires an active
23
- * (or trialing) subscription so a lapsed Enterprise org falls back to the path.
24
- */
25
- export function organizationHasVanitySubdomain(
28
+ function hasActiveEnterpriseTier(
26
29
  org: OrganizationEntitlementInput | null | undefined
27
30
  ): boolean {
28
31
  if (!org) return false;
@@ -36,18 +39,30 @@ export function organizationHasVanitySubdomain(
36
39
  );
37
40
  }
38
41
 
42
+ /**
43
+ * Vanity subdomains ({slug}.bash.community) and custom domains (white-label)
44
+ * are self-serve-eligible Enterprise perks — available whether Enterprise was
45
+ * bought via self-serve Stripe checkout or a sales-negotiated deal.
46
+ */
47
+ export function organizationHasVanitySubdomain(
48
+ org: OrganizationEntitlementInput | null | undefined
49
+ ): boolean {
50
+ return hasActiveEnterpriseTier(org);
51
+ }
52
+
39
53
  /**
40
54
  * Shared gate for the enterprise platform surface: org-scoped audit logs,
41
55
  * SSO/identity setup, procurement approval policy, API keys, webhooks, and
42
- * third-party integrations (Mailchimp/Salesforce). All are Enterprise-tier
43
- * perks today same predicate as {@link organizationHasVanitySubdomain}, kept
44
- * as a distinct name so the two can diverge later (e.g. Growth gets audit
45
- * logs before SSO) without touching every call site.
56
+ * third-party integrations (Mailchimp/Salesforce). Unlike the white-label
57
+ * perks above, this surface requires a sales-negotiated deal
58
+ * (`enterpriseSalesProvisioned`) self-serve $495/mo checkout alone does not
59
+ * unlock SSO, compliance, procurement, or API access, since those typically
60
+ * involve a security review or contract terms sales owns.
46
61
  */
47
62
  export function organizationHasEnterprisePlatformAccess(
48
63
  org: OrganizationEntitlementInput | null | undefined
49
64
  ): boolean {
50
- return organizationHasVanitySubdomain(org);
65
+ return hasActiveEnterpriseTier(org) && org?.enterpriseSalesProvisioned === true;
51
66
  }
52
67
 
53
68
  /** Org-admin audit log viewer + CSV export. */
@@ -84,3 +99,19 @@ export function organizationHasComplianceAccess(
84
99
  ): boolean {
85
100
  return organizationHasEnterprisePlatformAccess(org);
86
101
  }
102
+
103
+ /**
104
+ * Custom domains (white-label tier): real DNS (e.g. members.acme.com) provisioned
105
+ * via AWS CloudFront SaaS Manager Distribution Tenants, plus the "Powered by Bash"
106
+ * removal toggle. Same Enterprise + active/trialing gate as
107
+ * {@link organizationHasVanitySubdomain} for MVP — reuse the existing tier rather
108
+ * than invent a new pricing tier now. A dedicated "White-label" tier is a
109
+ * fast-follow if sales needs separate pricing. Self-serve-eligible: does NOT
110
+ * require {@link organizationHasEnterprisePlatformAccess}'s
111
+ * `enterpriseSalesProvisioned` flag.
112
+ */
113
+ export function organizationHasCustomDomainAccess(
114
+ org: OrganizationEntitlementInput | null | undefined
115
+ ): boolean {
116
+ return organizationHasVanitySubdomain(org);
117
+ }