@tribe-nest/forge 3.59.0 → 3.61.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 +2 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/CommunityContext.tsx +192 -0
- 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 +52 -40
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +345 -0
- package/src/data/queries/useCommunity.ts +450 -102
- 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/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- 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/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +28 -9
- package/src/i18n/de.json +891 -2
- package/src/i18n/en.json +891 -2
- package/src/index.ts +36 -7
- package/src/runtime/pages.spec.ts +36 -0
- package/src/runtime/pages.ts +52 -27
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -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/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- 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/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +212 -0
- 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/communityProvider.spec.tsx +151 -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/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- 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 +30 -3
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +96 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -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 +91 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -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/useCommunityAttachmentUpload.ts +7 -3
- 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 +21 -4
- package/src/ui/headless/memberHome/index.ts +11 -1
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- 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/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- 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 +28 -12
- package/src/ui/styled/AccountDashboard.tsx +754 -602
- 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/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/Paywall.tsx +1 -1
- 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 +6 -6
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- 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/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
- 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 +298 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
- 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/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +19 -0
- package/src/ui/styled/communities/shared.tsx +177 -0
- 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/CommunityComposer.spec.tsx +4 -1
- 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/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +253 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
- package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- 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/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Fragment, useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { FolderOpen } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
4
|
+
import { internalPath } from "../../../runtime/pages";
|
|
5
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
6
|
import { usePublicAuth } from "../../../contexts/PublicAuthContext";
|
|
5
|
-
import { ACCESS_TOKEN_KEY } from "../../../contexts/PublicAuthContext";
|
|
6
|
-
import { PUBLIC_REFRESH_TOKEN_KEY } from "../../../client/tokenStorage";
|
|
7
7
|
import { Loading } from "../Loading";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
rememberSignupEmail,
|
|
10
|
+
useAcceptWorkInvite,
|
|
11
|
+
useWorkInvite,
|
|
12
|
+
} from "../../headless/work/useWorkPortal";
|
|
13
|
+
import { usePortalButtons } from "../portal/shared";
|
|
14
|
+
import { requestAppInstall } from "../../headless/siteApp/appPrompts";
|
|
9
15
|
|
|
10
16
|
export interface WorkInviteAcceptProps {
|
|
11
17
|
/** Invite token from the email link (read from search params by the shell). */
|
|
@@ -13,96 +19,163 @@ export interface WorkInviteAcceptProps {
|
|
|
13
19
|
/** Project id from the email link. */
|
|
14
20
|
projectId?: string;
|
|
15
21
|
/** Base path for a project in the portal. The project link is
|
|
16
|
-
* `${projectHrefBase}/${projectId}`. Defaults to
|
|
22
|
+
* `${projectHrefBase}/${projectId}`. Defaults to `internalPath("account/projects")`. */
|
|
17
23
|
projectHrefBase?: string;
|
|
18
|
-
/**
|
|
24
|
+
/** The portal login. A `redirect` back to this invite is appended. Defaults to
|
|
25
|
+
* `internalPath("login")`. */
|
|
19
26
|
loginHref?: string;
|
|
27
|
+
/** The standard signup page. A `redirect` back to this invite is appended.
|
|
28
|
+
* Defaults to `internalPath("signup")`. */
|
|
29
|
+
signupHref?: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
const msg = (e: unknown) => (e as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
|
33
|
+
const status = (e: unknown) => (e as { response?: { status?: number } })?.response?.status;
|
|
34
|
+
|
|
35
|
+
/** Where the token waits while the person signs up or signs in (this tab only). */
|
|
36
|
+
const stashKey = (projectId: string) => `forge:work-invite:${projectId}`;
|
|
37
|
+
|
|
38
|
+
const readStash = (projectId?: string): string | undefined => {
|
|
39
|
+
if (!projectId || typeof window === "undefined") return undefined;
|
|
40
|
+
try {
|
|
41
|
+
return window.sessionStorage.getItem(stashKey(projectId)) ?? undefined;
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const writeStash = (projectId: string, token: string): boolean => {
|
|
48
|
+
try {
|
|
49
|
+
window.sessionStorage.setItem(stashKey(projectId), token);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const clearStash = (projectId: string) => {
|
|
57
|
+
try {
|
|
58
|
+
window.sessionStorage.removeItem(stashKey(projectId));
|
|
59
|
+
} catch {
|
|
60
|
+
// Storage unavailable: nothing was stashed.
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A translated sentence with some of its placeholders rendered in bold.
|
|
66
|
+
*
|
|
67
|
+
* The bundle holds the WHOLE sentence (`"You're signed in as {email}."`), so a
|
|
68
|
+
* translator can move the address wherever the language needs it. The
|
|
69
|
+
* placeholders are then swapped for nodes here instead of being filled as text.
|
|
70
|
+
*/
|
|
71
|
+
function Rich({ text, values }: { text: string; values: Record<string, string> }) {
|
|
72
|
+
const parts = text.split(/(\{[a-z_]+\})/g);
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
{parts.map((part, i) => {
|
|
76
|
+
const name = /^\{([a-z_]+)\}$/.exec(part)?.[1];
|
|
77
|
+
if (name && values[name] !== undefined) return <strong key={i}>{values[name]}</strong>;
|
|
78
|
+
return <Fragment key={i}>{part}</Fragment>;
|
|
79
|
+
})}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
23
83
|
|
|
24
84
|
/**
|
|
25
|
-
* Branded, AUTH-AWARE client-invite accept page.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
85
|
+
* Branded, AUTH-AWARE client-invite accept page.
|
|
86
|
+
*
|
|
87
|
+
* The invite never creates an account or sets a password: the link lands on the
|
|
88
|
+
* creator's own site, where page scripts can read it. So:
|
|
89
|
+
* - signed in AS the invited address: "Open project" accepts and opens it,
|
|
90
|
+
* - signed in as someone else: sign out and sign in as the invited address,
|
|
91
|
+
* - not signed in, address has an account: sign in, then come back here,
|
|
92
|
+
* - not signed in, no account yet: the normal signup (which verifies the
|
|
93
|
+
* address with an emailed code), prefilled with the invited address, then
|
|
94
|
+
* back here.
|
|
95
|
+
*
|
|
96
|
+
* The token is read once, kept in component state and this tab's session
|
|
97
|
+
* storage, and removed from the address bar so analytics fired later on the
|
|
98
|
+
* page never see it. Theme tokens only, so it inherits the artist's styling.
|
|
32
99
|
*/
|
|
33
100
|
export function WorkInviteAccept({
|
|
34
|
-
token,
|
|
101
|
+
token: tokenProp,
|
|
35
102
|
projectId,
|
|
36
|
-
projectHrefBase = "/
|
|
37
|
-
loginHref = "
|
|
103
|
+
projectHrefBase = internalPath("account/projects"),
|
|
104
|
+
loginHref = internalPath("login"),
|
|
105
|
+
signupHref = internalPath("signup"),
|
|
38
106
|
}: WorkInviteAcceptProps) {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
107
|
+
const t = useThemeTokens();
|
|
108
|
+
const tr = useForgeT();
|
|
109
|
+
const buttons = usePortalButtons();
|
|
110
|
+
const { isAuthenticated, user, logout } = usePublicAuth();
|
|
111
|
+
|
|
112
|
+
// Read once: the URL is about to lose it, and a re-render must not.
|
|
113
|
+
const [token] = useState<string | undefined>(() => tokenProp ?? readStash(projectId));
|
|
114
|
+
const [stashed, setStashed] = useState(false);
|
|
41
115
|
const { data: invite, isLoading, isError } = useWorkInvite(token, projectId);
|
|
42
116
|
const acceptInvite = useAcceptWorkInvite();
|
|
43
|
-
|
|
44
|
-
const [password, setPassword] = useState("");
|
|
45
|
-
const [confirm, setConfirm] = useState("");
|
|
46
117
|
const [error, setError] = useState("");
|
|
47
118
|
const [submitting, setSubmitting] = useState(false);
|
|
48
119
|
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!token || !projectId || typeof window === "undefined") return;
|
|
122
|
+
setStashed(writeStash(projectId, token));
|
|
123
|
+
try {
|
|
124
|
+
const url = new URL(window.location.href);
|
|
125
|
+
if (url.searchParams.has("token")) {
|
|
126
|
+
url.searchParams.delete("token");
|
|
127
|
+
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// A URL the browser cannot parse: leave it alone.
|
|
131
|
+
}
|
|
132
|
+
}, [token, projectId]);
|
|
133
|
+
|
|
49
134
|
const projectHref = `${projectHrefBase}/${projectId}`;
|
|
135
|
+
// Back to this page after signup or sign-in. The token rides in session
|
|
136
|
+
// storage when it can, so it never reaches the signup or login page's URL.
|
|
137
|
+
const backHere = `${internalPath("account/invite")}?project=${encodeURIComponent(projectId ?? "")}${
|
|
138
|
+
stashed || !token ? "" : `&token=${encodeURIComponent(token)}`
|
|
139
|
+
}`;
|
|
140
|
+
const withRedirect = (href: string) =>
|
|
141
|
+
`${href}${href.includes("?") ? "&" : "?"}redirect=${encodeURIComponent(backHere)}`;
|
|
50
142
|
|
|
51
|
-
const card:
|
|
143
|
+
const card: CSSProperties = {
|
|
52
144
|
maxWidth: 460,
|
|
53
145
|
margin: "40px auto 0",
|
|
54
146
|
padding: 28,
|
|
55
|
-
border: `1px solid ${
|
|
56
|
-
borderRadius:
|
|
57
|
-
color:
|
|
58
|
-
background:
|
|
59
|
-
|
|
60
|
-
const input: React.CSSProperties = {
|
|
61
|
-
width: "100%",
|
|
62
|
-
height: 42,
|
|
63
|
-
padding: "8px 12px",
|
|
64
|
-
borderRadius: theme.cornerRadius,
|
|
65
|
-
border: `1px solid ${theme.colors.primary}40`,
|
|
66
|
-
background: "transparent",
|
|
67
|
-
color: theme.colors.text,
|
|
68
|
-
outline: "none",
|
|
147
|
+
border: `1px solid ${t.border}`,
|
|
148
|
+
borderRadius: t.cornerRadius,
|
|
149
|
+
color: t.text,
|
|
150
|
+
background: t.background,
|
|
151
|
+
fontFamily: t.fontFamily,
|
|
69
152
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
fontWeight:
|
|
78
|
-
|
|
79
|
-
textAlign: "center",
|
|
80
|
-
textDecoration: "none",
|
|
81
|
-
display: "inline-flex",
|
|
82
|
-
alignItems: "center",
|
|
83
|
-
justifyContent: "center",
|
|
84
|
-
gap: 8,
|
|
153
|
+
const fullWidth: CSSProperties = { ...buttons.primary, width: "100%", minHeight: 44 };
|
|
154
|
+
const eyebrow: CSSProperties = { fontSize: 14, color: t.muted, fontWeight: 600 };
|
|
155
|
+
const heading: CSSProperties = {
|
|
156
|
+
margin: "6px 0 12px",
|
|
157
|
+
fontFamily: t.headingFontFamily,
|
|
158
|
+
fontSize: 24,
|
|
159
|
+
lineHeight: 1.2,
|
|
160
|
+
fontWeight: 600,
|
|
161
|
+
color: t.text,
|
|
85
162
|
};
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const heading: React.CSSProperties = { fontSize: 22, fontWeight: 800, margin: "6px 0 14px" };
|
|
94
|
-
const sub: React.CSSProperties = { fontSize: 14, color: `${theme.colors.text}b3`, marginBottom: 22 };
|
|
163
|
+
const sub: CSSProperties = { margin: "0 0 22px", fontSize: 15, lineHeight: 1.5, color: t.muted };
|
|
164
|
+
|
|
165
|
+
const signInLink = (
|
|
166
|
+
<a href={loginHref} style={buttons.quiet}>
|
|
167
|
+
{tr("forge.work_invite_accept.go_to_sign_in")}
|
|
168
|
+
</a>
|
|
169
|
+
);
|
|
95
170
|
|
|
96
171
|
if (!token || !projectId) {
|
|
97
172
|
return (
|
|
98
173
|
<div style={card} data-testid="work-invite-accept">
|
|
99
174
|
<h1 style={heading} data-testid="work-invite-invalid">
|
|
100
|
-
|
|
175
|
+
{tr("forge.work_invite_accept.missing_title")}
|
|
101
176
|
</h1>
|
|
102
|
-
<p style={sub}>
|
|
103
|
-
|
|
104
|
-
Go to sign in
|
|
105
|
-
</a>
|
|
177
|
+
<p style={sub}>{tr("forge.work_invite_accept.missing_body")}</p>
|
|
178
|
+
{signInLink}
|
|
106
179
|
</div>
|
|
107
180
|
);
|
|
108
181
|
}
|
|
@@ -119,153 +192,140 @@ export function WorkInviteAccept({
|
|
|
119
192
|
return (
|
|
120
193
|
<div style={card} data-testid="work-invite-accept">
|
|
121
194
|
<h1 style={heading} data-testid="work-invite-invalid">
|
|
122
|
-
|
|
195
|
+
{tr("forge.work_invite_accept.unavailable_title")}
|
|
123
196
|
</h1>
|
|
124
|
-
<p style={sub}>
|
|
125
|
-
|
|
126
|
-
Go to sign in
|
|
127
|
-
</a>
|
|
197
|
+
<p style={sub}>{tr("forge.work_invite_accept.unavailable_body")}</p>
|
|
198
|
+
{signInLink}
|
|
128
199
|
</div>
|
|
129
200
|
);
|
|
130
201
|
}
|
|
131
202
|
|
|
132
203
|
const invitedEmail = invite.email?.toLowerCase() ?? "";
|
|
133
204
|
const currentEmail = (user?.email ?? "").toLowerCase();
|
|
134
|
-
const signedInAsInvitee = isAuthenticated && currentEmail && currentEmail === invitedEmail;
|
|
205
|
+
const signedInAsInvitee = isAuthenticated && !!currentEmail && currentEmail === invitedEmail;
|
|
206
|
+
const signedInAsSomeoneElse = isAuthenticated && !!currentEmail && currentEmail !== invitedEmail;
|
|
135
207
|
|
|
136
|
-
const header = (
|
|
208
|
+
const header: ReactNode = (
|
|
137
209
|
<>
|
|
138
210
|
<div style={eyebrow}>{invite.profileName}</div>
|
|
139
211
|
<h1 style={heading} data-testid="work-invite-heading">
|
|
140
|
-
|
|
212
|
+
{tr("forge.work_invite_accept.heading", { project_name: invite.projectName })}
|
|
141
213
|
</h1>
|
|
142
214
|
</>
|
|
143
215
|
);
|
|
144
216
|
|
|
145
|
-
|
|
217
|
+
const errorLine = error ? (
|
|
218
|
+
<p role="alert" style={{ margin: "0 0 14px", fontSize: 14, color: t.text, fontWeight: 600 }} data-testid="work-invite-error">
|
|
219
|
+
{error}
|
|
220
|
+
</p>
|
|
221
|
+
) : null;
|
|
222
|
+
|
|
223
|
+
// Signed in AS the invited person: accepting links the project, then opens it.
|
|
146
224
|
if (signedInAsInvitee) {
|
|
225
|
+
const onOpen = async () => {
|
|
226
|
+
setError("");
|
|
227
|
+
setSubmitting(true);
|
|
228
|
+
try {
|
|
229
|
+
await acceptInvite.mutateAsync({ token, projectId });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// 404: the invite was already used, and this account already has the
|
|
232
|
+
// project. Anything else is a real failure to show.
|
|
233
|
+
if (status(err) !== 404) {
|
|
234
|
+
setError(msg(err) || tr("forge.work_invite_accept.generic_error"));
|
|
235
|
+
setSubmitting(false);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
clearStash(projectId);
|
|
240
|
+
// Rides the session to the project page, where the site app offers itself.
|
|
241
|
+
requestAppInstall("invite_accepted");
|
|
242
|
+
window.location.assign(projectHref);
|
|
243
|
+
};
|
|
147
244
|
return (
|
|
148
245
|
<div style={card} data-testid="work-invite-accept">
|
|
149
246
|
{header}
|
|
150
247
|
<p style={sub}>
|
|
151
|
-
<
|
|
152
|
-
<strong>{invite.email}</strong>. You already have access.
|
|
248
|
+
<Rich text={tr("forge.work_invite_accept.signed_in_as_invitee")} values={{ email: invite.email }} />
|
|
153
249
|
</p>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
250
|
+
{errorLine}
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
style={{ ...fullWidth, opacity: submitting ? 0.6 : 1 }}
|
|
254
|
+
disabled={submitting}
|
|
255
|
+
data-testid="work-invite-view-project"
|
|
256
|
+
onClick={onOpen}
|
|
257
|
+
>
|
|
258
|
+
<FolderOpen size={16} aria-hidden />{" "}
|
|
259
|
+
{submitting ? tr("forge.work_invite_accept.accepting") : tr("forge.work_invite_accept.view_project")}
|
|
260
|
+
</button>
|
|
157
261
|
</div>
|
|
158
262
|
);
|
|
159
263
|
}
|
|
160
264
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* portal ACCESS; it is not a credential reset for an account whose owner
|
|
164
|
-
* never asked for one — so there is no password form here, only a sign-in
|
|
165
|
-
* link. Accepting still runs, to consume the invite and link the contact.
|
|
166
|
-
*/
|
|
167
|
-
if (invite.requiresExistingLogin) {
|
|
265
|
+
// Signed in as somebody else: the invite is for one address only.
|
|
266
|
+
if (signedInAsSomeoneElse) {
|
|
168
267
|
return (
|
|
169
268
|
<div style={card} data-testid="work-invite-accept">
|
|
170
269
|
{header}
|
|
171
270
|
<p style={sub}>
|
|
172
|
-
<
|
|
271
|
+
<Rich
|
|
272
|
+
text={tr("forge.work_invite_accept.signed_in_as_other")}
|
|
273
|
+
values={{ current_email: user?.email ?? "", invited_email: invite.email }}
|
|
274
|
+
/>
|
|
173
275
|
</p>
|
|
174
276
|
<button
|
|
175
277
|
type="button"
|
|
176
|
-
style={
|
|
177
|
-
|
|
178
|
-
data-testid="work-invite-signin"
|
|
278
|
+
style={fullWidth}
|
|
279
|
+
data-testid="work-invite-switch-account"
|
|
179
280
|
onClick={async () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Consumes the invite and links the contact; returns no session.
|
|
183
|
-
await acceptInvite.mutateAsync({ token, projectId, password: "" });
|
|
184
|
-
} catch {
|
|
185
|
-
// Non-fatal: an already-consumed invite still leaves them able to
|
|
186
|
-
// sign in, which is the whole instruction on this screen.
|
|
187
|
-
}
|
|
188
|
-
window.location.assign(loginHref);
|
|
281
|
+
await logout();
|
|
282
|
+
window.location.assign(invite.needsAccount ? withRedirect(signupHref) : withRedirect(loginHref));
|
|
189
283
|
}}
|
|
190
284
|
>
|
|
191
|
-
|
|
285
|
+
{tr("forge.work_invite_accept.switch_account", { email: invite.email })}
|
|
192
286
|
</button>
|
|
193
287
|
</div>
|
|
194
288
|
);
|
|
195
289
|
}
|
|
196
290
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
setSubmitting(false);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
291
|
+
// Not signed in, and the address has no account: the normal signup first.
|
|
292
|
+
if (invite.needsAccount) {
|
|
293
|
+
return (
|
|
294
|
+
<div style={card} data-testid="work-invite-accept">
|
|
295
|
+
{header}
|
|
296
|
+
<p style={sub}>
|
|
297
|
+
<Rich text={tr("forge.work_invite_accept.create_account_intro")} values={{ email: invite.email }} />
|
|
298
|
+
</p>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
style={fullWidth}
|
|
302
|
+
data-testid="work-invite-signup"
|
|
303
|
+
onClick={() => {
|
|
304
|
+
rememberSignupEmail(invite.email);
|
|
305
|
+
window.location.assign(withRedirect(signupHref));
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
{tr("forge.work_invite_accept.create_account")}
|
|
309
|
+
</button>
|
|
310
|
+
<p style={{ ...sub, margin: "16px 0 0", fontSize: 14 }}>
|
|
311
|
+
<a href={withRedirect(loginHref)} style={buttons.quiet} data-testid="work-invite-signin">
|
|
312
|
+
{tr("forge.work_invite_accept.have_account")}
|
|
313
|
+
</a>
|
|
314
|
+
</p>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
227
318
|
|
|
319
|
+
// Not signed in, and the address already has an account: sign in, come back.
|
|
228
320
|
return (
|
|
229
321
|
<div style={card} data-testid="work-invite-accept">
|
|
230
322
|
{header}
|
|
231
323
|
<p style={sub}>
|
|
232
|
-
{
|
|
233
|
-
<>
|
|
234
|
-
You're signed in as <strong>{user?.email}</strong>, but this invite is for <strong>{invite.email}</strong>.
|
|
235
|
-
Set a password to continue as {invite.email}.
|
|
236
|
-
</>
|
|
237
|
-
) : (
|
|
238
|
-
<>
|
|
239
|
-
Set a password for <strong>{invite.email}</strong> to access your client portal.
|
|
240
|
-
</>
|
|
241
|
-
)}
|
|
324
|
+
<Rich text={tr("forge.work_invite_accept.existing_account")} values={{ email: invite.email }} />
|
|
242
325
|
</p>
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
</p>
|
|
247
|
-
)}
|
|
248
|
-
<form onSubmit={onAccept} style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
|
249
|
-
<input
|
|
250
|
-
type="password"
|
|
251
|
-
placeholder="Create a password"
|
|
252
|
-
value={password}
|
|
253
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
254
|
-
style={input}
|
|
255
|
-
data-testid="work-invite-password"
|
|
256
|
-
/>
|
|
257
|
-
<input
|
|
258
|
-
type="password"
|
|
259
|
-
placeholder="Confirm password"
|
|
260
|
-
value={confirm}
|
|
261
|
-
onChange={(e) => setConfirm(e.target.value)}
|
|
262
|
-
style={input}
|
|
263
|
-
data-testid="work-invite-confirm"
|
|
264
|
-
/>
|
|
265
|
-
<button type="submit" disabled={submitting} style={button} data-testid="work-invite-submit">
|
|
266
|
-
{submitting ? "Accepting…" : "Accept invite"}
|
|
267
|
-
</button>
|
|
268
|
-
</form>
|
|
326
|
+
<a href={withRedirect(loginHref)} style={fullWidth} data-testid="work-invite-signin">
|
|
327
|
+
{tr("forge.work_invite_accept.go_to_sign_in")}
|
|
328
|
+
</a>
|
|
269
329
|
</div>
|
|
270
330
|
);
|
|
271
331
|
}
|