@tribe-nest/forge 3.60.0 → 3.63.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/package.json +5 -2
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +20 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/seo/_tests/seo.spec.ts +326 -0
- package/src/seo/applySeoToHead.ts +98 -0
- package/src/seo/index.ts +9 -0
- package/src/seo/resolve.ts +211 -0
- package/src/seo/seoHead.ts +61 -0
- package/src/seo/siteSeoHead.ts +97 -0
- package/src/seo/types.ts +99 -0
- package/src/server/_tests/siteBootstrap.spec.ts +65 -0
- package/src/server/index.ts +33 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityHarness.tsx +10 -3
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/landing.spec.ts +39 -0
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
- package/src/utils/landing.ts +36 -10
- package/src/utils/metaEvents.ts +302 -0
- package/src/utils/structuredData.ts +339 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import type { PaginatedData } from "../../types/models";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -313,3 +313,26 @@ export function useCancelMyTicket() {
|
|
|
313
313
|
},
|
|
314
314
|
});
|
|
315
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Every ticket, a page at a time, for a list with a "Show more" button.
|
|
319
|
+
*
|
|
320
|
+
* Under the same `my-tickets` key prefix as `useMyTickets`, so the cancel and
|
|
321
|
+
* transfer mutations that invalidate that prefix refresh this too.
|
|
322
|
+
*/
|
|
323
|
+
export function useMyTicketsPaged(accountId?: string, limit = 20) {
|
|
324
|
+
const { client, profileId } = useForge();
|
|
325
|
+
return useInfiniteQuery({
|
|
326
|
+
queryKey: ["my-tickets", accountId, profileId, "paged", limit],
|
|
327
|
+
initialPageParam: 1,
|
|
328
|
+
queryFn: async ({ pageParam }) => {
|
|
329
|
+
const res = await client.get("/public/events/my-tickets", { params: { profileId, page: pageParam, limit } });
|
|
330
|
+
return res.data as PaginatedData<MyTicket>;
|
|
331
|
+
},
|
|
332
|
+
getNextPageParam: (last, pages) => {
|
|
333
|
+
const loaded = pages.reduce((sum, p) => sum + p.data.length, 0);
|
|
334
|
+
return loaded < last.total && last.data.length > 0 ? pages.length + 1 : undefined;
|
|
335
|
+
},
|
|
336
|
+
enabled: !!accountId && !!profileId && !!client,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Member notification headless hooks
|
|
6
|
+
* Member notification headless hooks: the FROZEN public contract that the
|
|
7
7
|
* apps/client PWA and every generated custom-code site consume. Speaks ONLY the
|
|
8
8
|
* `/public/notifications/*` API (see docs/archive/shipped/membership-notifications-initiative.md
|
|
9
9
|
* "API surface"). Moved here from apps/client so custom-code sites get push
|
|
@@ -56,6 +56,10 @@ export interface NotificationInboxItem {
|
|
|
56
56
|
url?: string;
|
|
57
57
|
actorName?: string;
|
|
58
58
|
actorAccountId?: string;
|
|
59
|
+
/** A `mod` notice about the member's own post or reply: what the moderator decided. */
|
|
60
|
+
outcome?: "approved" | "rejected";
|
|
61
|
+
/** The moderator's reason for a rejection, when one was given. */
|
|
62
|
+
reason?: string | null;
|
|
59
63
|
} | null;
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -218,7 +222,7 @@ export function useNotificationUnreadCount(enabled = true) {
|
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
/** Mark notifications read
|
|
225
|
+
/** Mark notifications read. Omit ids to mark ALL unread as read. */
|
|
222
226
|
export function useMarkNotificationsRead() {
|
|
223
227
|
const { client, profileId } = useForge();
|
|
224
228
|
const queryClient = useQueryClient();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { readMetaSignals } from "../../utils/metaEvents";
|
|
2
3
|
import { useMutation } from "@tanstack/react-query";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -74,7 +75,7 @@ export function useCreateOrder() {
|
|
|
74
75
|
|
|
75
76
|
return useMutation<CreateOrderResult, unknown, CreateOrderInput>({
|
|
76
77
|
mutationFn: async (input) => {
|
|
77
|
-
const res = await client.post("/public/orders", { ...input, profileId });
|
|
78
|
+
const res = await client.post("/public/orders", { ...input, profileId, metaSignals: readMetaSignals() });
|
|
78
79
|
return res.data;
|
|
79
80
|
},
|
|
80
81
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { shareRuntimeValue } from "../../runtime/registry";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Headless PWA install-prompt hook
|
|
5
|
+
* Headless PWA install-prompt hook: the versioned Forge primitive that powers
|
|
5
6
|
* `<InstallBanner/>` (forge/ui) and any custom install UI on a code site. Ported
|
|
6
7
|
* from apps/client/src/lib/hooks/usePWA.ts, plus iOS + standalone detection
|
|
7
8
|
* (Safari never fires `beforeinstallprompt`, so iOS needs the manual
|
|
@@ -22,7 +23,7 @@ export interface InstallPromptState {
|
|
|
22
23
|
isInstallable: boolean;
|
|
23
24
|
/** The app is running installed (standalone display-mode or iOS standalone). */
|
|
24
25
|
isInstalled: boolean;
|
|
25
|
-
/** iOS Safari
|
|
26
|
+
/** iOS Safari has no `beforeinstallprompt`: show the Add-to-Home-Screen guide. */
|
|
26
27
|
isIOS: boolean;
|
|
27
28
|
/** Currently rendered in standalone (installed) display mode. */
|
|
28
29
|
isStandalone: boolean;
|
|
@@ -47,8 +48,108 @@ function detectStandalone(): boolean {
|
|
|
47
48
|
return !!mm || iosStandalone;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// ---- The captured install prompt, shared ----------------------------------
|
|
52
|
+
//
|
|
53
|
+
// The browser fires `beforeinstallprompt` ONCE per page load, usually soon after
|
|
54
|
+
// it. A listener added later (a prompt mounted after the fan joins a community)
|
|
55
|
+
// would never hear it, so the event is captured the first time anything asks,
|
|
56
|
+
// and every reader shares the one captured event.
|
|
57
|
+
//
|
|
58
|
+
// Shared through the runtime registry, not a module variable: a site and the
|
|
59
|
+
// hosted `/i/` pages bundle each carry a copy of Forge, and the site's copy
|
|
60
|
+
// (`<PwaRegistration>`, at the root) is the one that hears the event.
|
|
61
|
+
|
|
62
|
+
interface InstallPromptStore {
|
|
63
|
+
prompt: BeforeInstallPromptEvent | null;
|
|
64
|
+
installed: boolean;
|
|
65
|
+
capturing: boolean;
|
|
66
|
+
listeners: Set<() => void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const store = (): InstallPromptStore =>
|
|
70
|
+
shareRuntimeValue<InstallPromptStore>("state:pwa-install-prompt", () => ({
|
|
71
|
+
prompt: null,
|
|
72
|
+
installed: false,
|
|
73
|
+
capturing: false,
|
|
74
|
+
listeners: new Set(),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const notifyPrompt = () => store().listeners.forEach((listener) => listener());
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start listening for `beforeinstallprompt` (and `appinstalled`). Idempotent and
|
|
81
|
+
* SSR-safe. `<PwaRegistration enabled>` calls it at mount so the event is held
|
|
82
|
+
* from the first page a visitor sees.
|
|
83
|
+
*/
|
|
84
|
+
export function captureInstallPrompt(): void {
|
|
85
|
+
const state = store();
|
|
86
|
+
if (state.capturing || typeof window === "undefined") return;
|
|
87
|
+
state.capturing = true;
|
|
88
|
+
window.addEventListener("beforeinstallprompt", (event: Event) => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
store().prompt = event as BeforeInstallPromptEvent;
|
|
91
|
+
notifyPrompt();
|
|
92
|
+
});
|
|
93
|
+
window.addEventListener("appinstalled", () => {
|
|
94
|
+
store().prompt = null;
|
|
95
|
+
store().installed = true;
|
|
96
|
+
notifyPrompt();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True when a native install prompt is held and can be shown. */
|
|
101
|
+
export function hasStoredInstallPrompt(): boolean {
|
|
102
|
+
return !!store().prompt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True once the app was installed during this page's life. */
|
|
106
|
+
export function wasInstalledThisSession(): boolean {
|
|
107
|
+
return store().installed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Be told when the held prompt arrives, is used, or the app is installed. */
|
|
111
|
+
export function subscribeInstallPrompt(listener: () => void): () => void {
|
|
112
|
+
store().listeners.add(listener);
|
|
113
|
+
return () => {
|
|
114
|
+
store().listeners.delete(listener);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Show the held native prompt. Resolves true when the visitor accepted. A
|
|
120
|
+
* prompt can be shown once, so it is released either way.
|
|
121
|
+
*/
|
|
122
|
+
export async function promptStoredInstall(): Promise<boolean> {
|
|
123
|
+
const state = store();
|
|
124
|
+
const event = state.prompt;
|
|
125
|
+
if (!event) return false;
|
|
126
|
+
state.prompt = null;
|
|
127
|
+
notifyPrompt();
|
|
128
|
+
try {
|
|
129
|
+
await event.prompt();
|
|
130
|
+
const { outcome } = await event.userChoice;
|
|
131
|
+
if (outcome === "accepted") {
|
|
132
|
+
state.installed = true;
|
|
133
|
+
notifyPrompt();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Test seam: forget whatever was captured, or hold `event` as if the browser had fired it. */
|
|
143
|
+
export function resetInstallPromptForTests(event: Event | null = null): void {
|
|
144
|
+
const state = store();
|
|
145
|
+
state.prompt = event as BeforeInstallPromptEvent | null;
|
|
146
|
+
state.installed = false;
|
|
147
|
+
notifyPrompt();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { detectIOS as isIOSDevice, detectStandalone as isStandaloneDisplay };
|
|
151
|
+
|
|
50
152
|
export function useInstallPrompt(): InstallPromptState {
|
|
51
|
-
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
52
153
|
const [isInstallable, setIsInstallable] = useState(false);
|
|
53
154
|
const [isInstalled, setIsInstalled] = useState(false);
|
|
54
155
|
const [isIOS, setIsIOS] = useState(false);
|
|
@@ -61,41 +162,16 @@ export function useInstallPrompt(): InstallPromptState {
|
|
|
61
162
|
setIsStandalone(standalone);
|
|
62
163
|
if (standalone) setIsInstalled(true);
|
|
63
164
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
const handleAppInstalled = () => {
|
|
70
|
-
setIsInstalled(true);
|
|
71
|
-
setIsInstallable(false);
|
|
72
|
-
setDeferredPrompt(null);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
76
|
-
window.addEventListener("appinstalled", handleAppInstalled);
|
|
77
|
-
return () => {
|
|
78
|
-
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
79
|
-
window.removeEventListener("appinstalled", handleAppInstalled);
|
|
165
|
+
captureInstallPrompt();
|
|
166
|
+
const sync = () => {
|
|
167
|
+
setIsInstallable(hasStoredInstallPrompt());
|
|
168
|
+
if (wasInstalledThisSession()) setIsInstalled(true);
|
|
80
169
|
};
|
|
170
|
+
sync();
|
|
171
|
+
return subscribeInstallPrompt(sync);
|
|
81
172
|
}, []);
|
|
82
173
|
|
|
83
|
-
const promptInstall = useCallback(
|
|
84
|
-
if (!deferredPrompt) return false;
|
|
85
|
-
try {
|
|
86
|
-
await deferredPrompt.prompt();
|
|
87
|
-
const { outcome } = await deferredPrompt.userChoice;
|
|
88
|
-
if (outcome === "accepted") {
|
|
89
|
-
setIsInstalled(true);
|
|
90
|
-
setIsInstallable(false);
|
|
91
|
-
setDeferredPrompt(null);
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}, [deferredPrompt]);
|
|
174
|
+
const promptInstall = useCallback(() => promptStoredInstall(), []);
|
|
99
175
|
|
|
100
176
|
return { isInstallable, isInstalled, isIOS, isStandalone, promptInstall };
|
|
101
177
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
4
|
+
import { internalPath } from "../../runtime/pages";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The site app: the installable app every website is (one per origin).
|
|
8
|
+
*
|
|
9
|
+
* Speaks `GET /public/websites/app?host=<host>`, which answers for the website
|
|
10
|
+
* that owns the host the visitor is on. Contract:
|
|
11
|
+
* docs/proposals/site-app-and-website-menu.md, sections 2 and 3.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The built-in sections of the app home, in their default order. */
|
|
15
|
+
export const SITE_APP_SECTION_KEYS = [
|
|
16
|
+
"needs_you",
|
|
17
|
+
"portal",
|
|
18
|
+
"communities",
|
|
19
|
+
"coming_up",
|
|
20
|
+
"learning",
|
|
21
|
+
"explore_communities",
|
|
22
|
+
"events",
|
|
23
|
+
"courses",
|
|
24
|
+
"shop",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type SiteAppSectionKey = (typeof SITE_APP_SECTION_KEYS)[number];
|
|
28
|
+
|
|
29
|
+
/** What may sit in the app bar between Home and More. */
|
|
30
|
+
export const SITE_APP_BAR_KEYS = ["communities", "portal", "inbox", "events", "courses", "shop"] as const;
|
|
31
|
+
|
|
32
|
+
export type SiteAppBarKey = (typeof SITE_APP_BAR_KEYS)[number];
|
|
33
|
+
|
|
34
|
+
/** How many configured items fit between Home and More. */
|
|
35
|
+
export const SITE_APP_BAR_MAX_ITEMS = 3;
|
|
36
|
+
|
|
37
|
+
export type SiteAppHomeSection =
|
|
38
|
+
| { type: "builtin"; key: SiteAppSectionKey; visible: boolean }
|
|
39
|
+
| { type: "link"; id: string; label: string; href: string; visible: boolean };
|
|
40
|
+
|
|
41
|
+
/** The public answer, exactly as section 3 of the contract shapes it. */
|
|
42
|
+
export interface SiteApp {
|
|
43
|
+
websiteId: string;
|
|
44
|
+
profileId: string;
|
|
45
|
+
appName: string;
|
|
46
|
+
shortName: string;
|
|
47
|
+
iconUrl: string | null;
|
|
48
|
+
themeColor: string | null;
|
|
49
|
+
backgroundColor: string | null;
|
|
50
|
+
/** `/i/app`, or the creator's own start page. Home everywhere. */
|
|
51
|
+
startUrl: string;
|
|
52
|
+
/** Only the VISIBLE sections, in the configured order. */
|
|
53
|
+
homeSections: SiteAppHomeSection[];
|
|
54
|
+
barItems: SiteAppBarKey[];
|
|
55
|
+
installPrompt: boolean;
|
|
56
|
+
pushPrompt: boolean;
|
|
57
|
+
activeVersionId: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Where the app starts when the creator has not chosen a page of their own. */
|
|
61
|
+
export const SITE_APP_DEFAULT_START = internalPath("app");
|
|
62
|
+
|
|
63
|
+
/** Visible by default; `events` and `courses` are off until the creator turns them on. */
|
|
64
|
+
const DEFAULT_HIDDEN: ReadonlySet<SiteAppSectionKey> = new Set(["events", "courses"]);
|
|
65
|
+
|
|
66
|
+
/** The visible home sections when no settings answer: the contract's defaults. */
|
|
67
|
+
export const SITE_APP_DEFAULT_SECTIONS: readonly SiteAppHomeSection[] = SITE_APP_SECTION_KEYS.filter(
|
|
68
|
+
(key) => !DEFAULT_HIDDEN.has(key),
|
|
69
|
+
).map((key) => ({ type: "builtin", key, visible: true }) as const);
|
|
70
|
+
|
|
71
|
+
export const SITE_APP_DEFAULT_BAR: readonly SiteAppBarKey[] = ["communities", "portal", "inbox"];
|
|
72
|
+
|
|
73
|
+
export const siteAppKeys = {
|
|
74
|
+
byHost: (host: string | null) => ["site-app", host] as const,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The host the visitor is on, read after mount.
|
|
79
|
+
*
|
|
80
|
+
* Not during render: the server has no `window`, and reading it there would
|
|
81
|
+
* render one thing on the server and another on the first client pass.
|
|
82
|
+
*/
|
|
83
|
+
export function useCurrentHost(): string | null {
|
|
84
|
+
const [host, setHost] = useState<string | null>(null);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (typeof window !== "undefined") setHost(window.location.host || null);
|
|
87
|
+
}, []);
|
|
88
|
+
return host;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* This website's app settings, for the host the visitor is on.
|
|
93
|
+
*
|
|
94
|
+
* An unknown host (a preview, the edit sandbox) is a 404, which is an answer
|
|
95
|
+
* rather than a fault: it is not retried, and every reader falls back to the
|
|
96
|
+
* defaults (`useAppHomeHref` to `/i/app`).
|
|
97
|
+
*/
|
|
98
|
+
export function useSiteApp(opts: { host?: string | null; enabled?: boolean } = {}) {
|
|
99
|
+
const { client } = useForge();
|
|
100
|
+
const currentHost = useCurrentHost();
|
|
101
|
+
const host = opts.host ?? currentHost;
|
|
102
|
+
return useQuery<SiteApp>({
|
|
103
|
+
queryKey: siteAppKeys.byHost(host),
|
|
104
|
+
queryFn: async () => (await client.get("/public/websites/app", { params: { host } })).data as SiteApp,
|
|
105
|
+
enabled: !!client && !!host && opts.enabled !== false,
|
|
106
|
+
retry: false,
|
|
107
|
+
// Settings change rarely, and every page with an app bar reads them.
|
|
108
|
+
staleTime: 5 * 60_000,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Where "Home" is: the creator's start page when they chose one, else `/i/app`.
|
|
114
|
+
*
|
|
115
|
+
* The Home rule (contract section 4): the app bar's Home, the community
|
|
116
|
+
* header's logo, and where sign-in lands by default all read this, so a creator
|
|
117
|
+
* who moves Home moves it everywhere at once.
|
|
118
|
+
*/
|
|
119
|
+
export function useAppHomeHref(): string {
|
|
120
|
+
const { data } = useSiteApp();
|
|
121
|
+
return resolveAppHomeHref(data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The pure half of `useAppHomeHref`, for a caller that already holds the settings. */
|
|
125
|
+
export function resolveAppHomeHref(app: Pick<SiteApp, "startUrl"> | null | undefined): string {
|
|
126
|
+
const start = app?.startUrl?.trim();
|
|
127
|
+
// Only a root-relative path is a Home: anything else would send the member off-site.
|
|
128
|
+
if (!start || !start.startsWith("/") || start.startsWith("//")) return SITE_APP_DEFAULT_START;
|
|
129
|
+
return start;
|
|
130
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { readMetaSignals } from "../../utils/metaEvents";
|
|
2
3
|
import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
3
4
|
import { useMutation } from "@tanstack/react-query";
|
|
4
5
|
import { usePublicAuth } from "../../contexts/PublicAuthContext";
|
|
@@ -68,7 +69,11 @@ export function useCreateSubscription() {
|
|
|
68
69
|
|
|
69
70
|
return useMutation<CreateSubscriptionResult, unknown, CreateSubscriptionInput>({
|
|
70
71
|
mutationFn: async (body) => {
|
|
71
|
-
const res = await client.post("/public/payments/subscriptions", {
|
|
72
|
+
const res = await client.post("/public/payments/subscriptions", {
|
|
73
|
+
...body,
|
|
74
|
+
profileId,
|
|
75
|
+
metaSignals: readMetaSignals(),
|
|
76
|
+
});
|
|
72
77
|
return res.data;
|
|
73
78
|
},
|
|
74
79
|
});
|