@tribe-nest/forge 3.59.0 → 3.60.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 +1 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/data/queries/useChat.ts +42 -39
- package/src/data/queries/useCommunities.ts +343 -0
- package/src/data/queries/useCommunity.ts +93 -89
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSubscriptions.ts +22 -8
- package/src/i18n/de.json +145 -1
- package/src/i18n/en.json +145 -1
- package/src/index.ts +19 -7
- package/src/runtime/pages.spec.ts +31 -0
- package/src/runtime/pages.ts +21 -22
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +205 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +382 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +286 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/auth/useSignupForm.ts +10 -2
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +55 -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/useCommunityHome.ts +88 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +444 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +136 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/index.ts +8 -4
- package/src/ui/headless/memberHome/index.ts +1 -1
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +44 -42
- 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/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/index.ts +17 -12
- package/src/ui/styled/AccountDashboard.tsx +48 -256
- 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/Paywall.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +1 -1
- 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/CourseCallsSection.spec.tsx +7 -1
- 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/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +140 -0
- package/src/ui/styled/communities/CommunityHome.tsx +175 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +444 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +133 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +89 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/index.ts +10 -0
- package/src/ui/styled/communities/shared.tsx +149 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/forge-utilities.css +19 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +142 -10
- package/src/ui/styled/members/MemberHomeLayout.tsx +35 -17
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- 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/utils/_tests/safeRedirect.spec.ts +2 -2
- 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
package/src/i18n/en.json
CHANGED
|
@@ -655,6 +655,118 @@
|
|
|
655
655
|
"forge.cohort_page.date_range": "{start} to {end}",
|
|
656
656
|
"forge.cohort_page.date_tbd": "TBD",
|
|
657
657
|
"forge.cohort_page.not_found": "Cohort not found.",
|
|
658
|
+
"forge.communities_list.empty": "There are no communities to join yet.",
|
|
659
|
+
"forge.communities_list.heading": "Communities",
|
|
660
|
+
"forge.communities_list.member_count": "{count} members",
|
|
661
|
+
"forge.community_about.apply": "Apply to join",
|
|
662
|
+
"forge.community_about.current_tier": "Your current tier",
|
|
663
|
+
"forge.community_about.go_inside": "Go to the community",
|
|
664
|
+
"forge.community_about.join": "Join",
|
|
665
|
+
"forge.community_about.join_free": "Join free",
|
|
666
|
+
"forge.community_about.join_from": "Join from {amount}/mo",
|
|
667
|
+
"forge.community_about.join_from_yearly": "Join from {amount}/yr",
|
|
668
|
+
"forge.community_about.join_tier": "Join",
|
|
669
|
+
"forge.community_about.member_count": "{count} members",
|
|
670
|
+
"forge.community_about.not_found": "This community could not be found.",
|
|
671
|
+
"forge.community_about.pending": "Your application is being reviewed.",
|
|
672
|
+
"forge.community_about.tiers_title": "Membership tiers",
|
|
673
|
+
"forge.community_about.view_application": "View your application",
|
|
674
|
+
"forge.community_home.confirming": "Confirming your membership...",
|
|
675
|
+
"forge.community_home.dismiss": "Dismiss",
|
|
676
|
+
"forge.community_home.join": "Join",
|
|
677
|
+
"forge.community_home.membership": "Membership",
|
|
678
|
+
"forge.community_home.not_found": "This community could not be found.",
|
|
679
|
+
"forge.community_home.upgrade": "Upgrade",
|
|
680
|
+
"forge.community_home.welcome": "Welcome to {community_name}!",
|
|
681
|
+
"forge.community_join_sheet.accept_terms": "I accept the Terms and Privacy Policy",
|
|
682
|
+
"forge.community_join_sheet.already_member": "You are already a member.",
|
|
683
|
+
"forge.community_join_sheet.amount_below_minimum": "The minimum is {minimum}.",
|
|
684
|
+
"forge.community_join_sheet.amount_label": "Choose your amount (minimum {minimum})",
|
|
685
|
+
"forge.community_join_sheet.amount_not_positive": "Enter an amount above zero.",
|
|
686
|
+
"forge.community_join_sheet.answer_required": "Please answer: {question}",
|
|
687
|
+
"forge.community_join_sheet.apply": "Apply to join",
|
|
688
|
+
"forge.community_join_sheet.aria_close": "Close",
|
|
689
|
+
"forge.community_join_sheet.back": "Back",
|
|
690
|
+
"forge.community_join_sheet.billing_cycle": "Billing cycle",
|
|
691
|
+
"forge.community_join_sheet.card_application": "Add a card for your application",
|
|
692
|
+
"forge.community_join_sheet.card_application_help": "Your card is saved, not charged. You are charged only if your application is approved.",
|
|
693
|
+
"forge.community_join_sheet.card_application_help_label": "When is my card charged?",
|
|
694
|
+
"forge.community_join_sheet.card_payment": "Pay {amount}",
|
|
695
|
+
"forge.community_join_sheet.card_trial": "Add a card to start your free trial",
|
|
696
|
+
"forge.community_join_sheet.change_tier": "Change tier",
|
|
697
|
+
"forge.community_join_sheet.choose_tier": "Choose a tier",
|
|
698
|
+
"forge.community_join_sheet.code": "6-digit code",
|
|
699
|
+
"forge.community_join_sheet.code_sent": "We emailed a 6-digit code to {email}.",
|
|
700
|
+
"forge.community_join_sheet.continue": "Continue",
|
|
701
|
+
"forge.community_join_sheet.email": "Email",
|
|
702
|
+
"forge.community_join_sheet.email_mismatch": "This invite is for {email}. You are signed in as {signed_in_email}.",
|
|
703
|
+
"forge.community_join_sheet.first_name": "First name",
|
|
704
|
+
"forge.community_join_sheet.invite_expired": "This invite has expired. You can still join below.",
|
|
705
|
+
"forge.community_join_sheet.invite_not_found": "This invite link is not valid. You can still join below.",
|
|
706
|
+
"forge.community_join_sheet.invite_revoked": "This invite was withdrawn. You can still join below.",
|
|
707
|
+
"forge.community_join_sheet.invite_used": "This invite has already been used. You can still join below.",
|
|
708
|
+
"forge.community_join_sheet.invite_used_up": "This invite link has no places left. You can still join below.",
|
|
709
|
+
"forge.community_join_sheet.join_for": "Join for {amount}",
|
|
710
|
+
"forge.community_join_sheet.join_free": "Join free",
|
|
711
|
+
"forge.community_join_sheet.joined_title": "You're in!",
|
|
712
|
+
"forge.community_join_sheet.joining": "Joining...",
|
|
713
|
+
"forge.community_join_sheet.last_name": "Last name",
|
|
714
|
+
"forge.community_join_sheet.mode_signin": "Sign in",
|
|
715
|
+
"forge.community_join_sheet.mode_signup": "Create account",
|
|
716
|
+
"forge.community_join_sheet.monthly": "Monthly",
|
|
717
|
+
"forge.community_join_sheet.monthly_price": "Monthly, {amount}",
|
|
718
|
+
"forge.community_join_sheet.password": "Password",
|
|
719
|
+
"forge.community_join_sheet.pay": "Pay {amount}",
|
|
720
|
+
"forge.community_join_sheet.pending_body": "Your application is being reviewed. We'll let you know when it is decided.",
|
|
721
|
+
"forge.community_join_sheet.pending_title": "Application sent",
|
|
722
|
+
"forge.community_join_sheet.resend": "Resend code",
|
|
723
|
+
"forge.community_join_sheet.select_placeholder": "Select...",
|
|
724
|
+
"forge.community_join_sheet.sign_in_as": "Sign in as {email}",
|
|
725
|
+
"forge.community_join_sheet.start_trial": "Start free trial",
|
|
726
|
+
"forge.community_join_sheet.taking_you_inside": "Taking you inside...",
|
|
727
|
+
"forge.community_join_sheet.title": "Join {community_name}",
|
|
728
|
+
"forge.community_join_sheet.title_plain": "Join",
|
|
729
|
+
"forge.community_join_sheet.trial_line": "{days}-day free trial, first charge on {date}",
|
|
730
|
+
"forge.community_join_sheet.verify": "Verify",
|
|
731
|
+
"forge.community_join_sheet.working": "Please wait...",
|
|
732
|
+
"forge.community_join_sheet.yearly": "Yearly",
|
|
733
|
+
"forge.community_join_sheet.yearly_price": "Yearly, {amount}",
|
|
734
|
+
"forge.community_membership_menu.aria_close": "Close",
|
|
735
|
+
"forge.community_membership_menu.cancel": "Cancel membership",
|
|
736
|
+
"forge.community_membership_menu.cancel_confirm": "Cancel membership",
|
|
737
|
+
"forge.community_membership_menu.cancel_confirm_now": "Your membership ends now.",
|
|
738
|
+
"forge.community_membership_menu.cancel_confirm_title": "Cancel your membership?",
|
|
739
|
+
"forge.community_membership_menu.cancel_confirm_until": "You keep access until {date}. It will not renew.",
|
|
740
|
+
"forge.community_membership_menu.cancel_keep": "Keep membership",
|
|
741
|
+
"forge.community_membership_menu.downgrade_to": "Switch to {tier_name} at renewal",
|
|
742
|
+
"forge.community_membership_menu.ending": "Ending",
|
|
743
|
+
"forge.community_membership_menu.ends_on": "Ends on {date}",
|
|
744
|
+
"forge.community_membership_menu.payment_failed": "Payment failed",
|
|
745
|
+
"forge.community_membership_menu.pending_downgrade": "Switching to {tier_name} on {date}.",
|
|
746
|
+
"forge.community_membership_menu.renews_on": "Renews on {date}",
|
|
747
|
+
"forge.community_membership_menu.title": "Your membership",
|
|
748
|
+
"forge.community_membership_menu.trigger": "Membership",
|
|
749
|
+
"forge.community_membership_menu.undo_downgrade": "Keep my current tier",
|
|
750
|
+
"forge.community_membership_menu.update_card": "Update payment method",
|
|
751
|
+
"forge.community_membership_menu.upgrade": "Upgrade",
|
|
752
|
+
"forge.community_pending.body": "Your application is being reviewed. You'll get access as soon as it is approved.",
|
|
753
|
+
"forge.community_pending.body_since": "You applied on {date}. You'll get access as soon as it is approved.",
|
|
754
|
+
"forge.community_pending.title": "Your application is being reviewed",
|
|
755
|
+
"forge.community_upgrade_sheet.aria_close": "Close",
|
|
756
|
+
"forge.community_upgrade_sheet.back": "Back",
|
|
757
|
+
"forge.community_upgrade_sheet.charge_now": "You pay {amount} now.",
|
|
758
|
+
"forge.community_upgrade_sheet.charge_prorated": "You pay {amount} now for the rest of this period.",
|
|
759
|
+
"forge.community_upgrade_sheet.confirm": "Upgrade now",
|
|
760
|
+
"forge.community_upgrade_sheet.continue_to_card": "Continue to card",
|
|
761
|
+
"forge.community_upgrade_sheet.current": "You are on {tier_name}.",
|
|
762
|
+
"forge.community_upgrade_sheet.done": "Upgraded!",
|
|
763
|
+
"forge.community_upgrade_sheet.none": "You are already on the highest tier.",
|
|
764
|
+
"forge.community_upgrade_sheet.pay": "Pay and upgrade",
|
|
765
|
+
"forge.community_upgrade_sheet.price_monthly": "{amount}/mo",
|
|
766
|
+
"forge.community_upgrade_sheet.price_yearly": "{amount}/yr",
|
|
767
|
+
"forge.community_upgrade_sheet.requires_card": "Upgrading asks for your card again and replaces your current subscription.",
|
|
768
|
+
"forge.community_upgrade_sheet.title": "Upgrade",
|
|
769
|
+
"forge.community_upgrade_sheet.unlock_with": "Unlock with {tier_name}",
|
|
658
770
|
"forge.confirm_subscription.back_to_site": "Back to site",
|
|
659
771
|
"forge.confirm_subscription.expired_body": "This confirmation link is no longer valid. Try subscribing again.",
|
|
660
772
|
"forge.confirm_subscription.expired_title": "Link expired",
|
|
@@ -1167,6 +1279,19 @@
|
|
|
1167
1279
|
"forge.login_form.verify_submit": "Verify & Sign In",
|
|
1168
1280
|
"forge.login_form.verify_title": "Verify your email",
|
|
1169
1281
|
"forge.login_form.verifying": "Verifying…",
|
|
1282
|
+
"forge.member_home_layout.nav_label": "Community",
|
|
1283
|
+
"forge.member_home_layout.tab_calendar": "Calendar",
|
|
1284
|
+
"forge.member_home_layout.tab_chat": "Chat",
|
|
1285
|
+
"forge.member_home_layout.tab_collections": "Collections",
|
|
1286
|
+
"forge.member_home_layout.tab_leaderboard": "Leaderboard",
|
|
1287
|
+
"forge.member_home_layout.tab_notifications": "Notifications",
|
|
1288
|
+
"forge.member_home_layout.tab_posts": "Posts",
|
|
1289
|
+
"forge.member_home_layout.tab_spaces": "Spaces",
|
|
1290
|
+
"forge.member_post_card.locked_body": "Join to access this content",
|
|
1291
|
+
"forge.member_post_card.locked_body_tier": "Unlock with {tier_name} to access this content",
|
|
1292
|
+
"forge.member_post_card.locked_title": "Content locked",
|
|
1293
|
+
"forge.member_post_card.unlock": "Unlock",
|
|
1294
|
+
"forge.member_post_card.unlock_with": "Unlock with {tier_name}",
|
|
1170
1295
|
"forge.membership_checkout.accept_invite": "Accept invite",
|
|
1171
1296
|
"forge.membership_checkout.amount_below_minimum": "Enter {minimum} or more.",
|
|
1172
1297
|
"forge.membership_checkout.amount_not_positive": "Enter an amount greater than zero.",
|
|
@@ -1203,7 +1328,7 @@
|
|
|
1203
1328
|
"forge.membership_checkout.empty_title": "No memberships available",
|
|
1204
1329
|
"forge.membership_checkout.field_required": "This field is required",
|
|
1205
1330
|
"forge.membership_checkout.free": "Free",
|
|
1206
|
-
"forge.membership_checkout.
|
|
1331
|
+
"forge.membership_checkout.go_to_community": "Go to the community",
|
|
1207
1332
|
"forge.membership_checkout.invite_dead_body": "It may have expired, been used already, or been replaced by a newer invite. Ask us for a new one.",
|
|
1208
1333
|
"forge.membership_checkout.invite_dead_title": "This invite is no longer valid",
|
|
1209
1334
|
"forge.membership_checkout.invite_email_mismatch": "You're signed in as {signed_in_email}. Sign in with {email} to accept this invite.",
|
|
@@ -1235,6 +1360,7 @@
|
|
|
1235
1360
|
"forge.membership_tier_callout.invite_for_email": "This invite is for {email}. Sign in or create an account with that address.",
|
|
1236
1361
|
"forge.membership_tier_callout.join_title": "Join {tier_name}",
|
|
1237
1362
|
"forge.membership_tier_callout.more_benefits": "+{count} more benefits",
|
|
1363
|
+
"forge.membership_tier_callout.view_community": "Part of {community_name}",
|
|
1238
1364
|
"forge.membership_tiers.accept_invite": "Accept invite",
|
|
1239
1365
|
"forge.membership_tiers.application_approved": "Approved, finishing up",
|
|
1240
1366
|
"forge.membership_tiers.application_declined": "Not approved",
|
|
@@ -1255,6 +1381,14 @@
|
|
|
1255
1381
|
"forge.membership_tiers.withdraw": "Withdraw application",
|
|
1256
1382
|
"forge.membership_tiers.withdraw_error": "We couldn't withdraw your application. Please try again.",
|
|
1257
1383
|
"forge.membership_tiers.withdrawing": "Withdrawing…",
|
|
1384
|
+
"forge.my_memberships_list.cycle_month": "month",
|
|
1385
|
+
"forge.my_memberships_list.cycle_year": "year",
|
|
1386
|
+
"forge.my_memberships_list.empty": "You are not a member of any community yet.",
|
|
1387
|
+
"forge.my_memberships_list.ends_on": "Ends on {date}",
|
|
1388
|
+
"forge.my_memberships_list.open": "Open community",
|
|
1389
|
+
"forge.my_memberships_list.price": "{amount} per {cycle}",
|
|
1390
|
+
"forge.my_memberships_list.renews_on": "Renews on {date}",
|
|
1391
|
+
"forge.my_memberships_list.trial_until": "Free trial until {date}",
|
|
1258
1392
|
"forge.offer_button.buy_now": "Buy now",
|
|
1259
1393
|
"forge.offer_button.confirm_email": "Confirm email",
|
|
1260
1394
|
"forge.offer_button.continue_to_payment": "Continue to payment",
|
|
@@ -1310,6 +1444,16 @@
|
|
|
1310
1444
|
"forge.podcast_show.empty_title": "No episodes yet.",
|
|
1311
1445
|
"forge.podcast_show.not_found": "Podcast not found.",
|
|
1312
1446
|
"forge.podcast_show.now_playing": "Now playing",
|
|
1447
|
+
"forge.post_collection_detail.locked_body": "Join to access this content",
|
|
1448
|
+
"forge.post_collection_detail.locked_body_tier": "Unlock with {tier_name} to access this content",
|
|
1449
|
+
"forge.post_collection_detail.locked_title": "Content locked",
|
|
1450
|
+
"forge.post_collection_detail.unlock": "Unlock",
|
|
1451
|
+
"forge.post_collection_detail.unlock_with": "Unlock with {tier_name}",
|
|
1452
|
+
"forge.post_collections_grid.locked_body": "Join to access this content",
|
|
1453
|
+
"forge.post_collections_grid.locked_body_tier": "Unlock with {tier_name} to access this content",
|
|
1454
|
+
"forge.post_collections_grid.locked_title": "Content locked",
|
|
1455
|
+
"forge.post_collections_grid.unlock": "Unlock",
|
|
1456
|
+
"forge.post_collections_grid.unlock_with": "Unlock with {tier_name}",
|
|
1313
1457
|
"forge.posts_feed.empty_body": "No posts yet.",
|
|
1314
1458
|
"forge.presale_code.checking": "Checking…",
|
|
1315
1459
|
"forge.presale_code.input_placeholder": "Enter code",
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Forge
|
|
1
|
+
// Forge: the headless React SDK for TribeNest creator sites.
|
|
2
2
|
//
|
|
3
3
|
// Phase 1 (the spine): client factory, root provider, core types.
|
|
4
4
|
// Subsequent phases add providers (auth/cart/audio), data hooks, flow hooks,
|
|
5
|
-
// headless blocks, and SEO
|
|
5
|
+
// headless blocks, and SEO: see docs/code-websites-initiative.md.
|
|
6
6
|
|
|
7
7
|
export { createForgeClient, getForgeWebSocketUrl } from "./client/createForgeClient";
|
|
8
8
|
export type { ForgeClientOptions } from "./client/createForgeClient";
|
|
@@ -43,12 +43,12 @@ export {
|
|
|
43
43
|
// All domain types + enums/consts (models) and WebPage.
|
|
44
44
|
export * from "./types";
|
|
45
45
|
|
|
46
|
-
// Providers (Phase 2
|
|
46
|
+
// Providers (Phase 2: moving from frontend-shared into Forge)
|
|
47
47
|
export { AudioPlayerProvider, useAudioPlayer } from "./contexts/AudioPlayerContext";
|
|
48
48
|
export type { AudioTrack } from "./contexts/AudioPlayerContext";
|
|
49
49
|
export { CartProvider, useCart } from "./contexts/CartContext";
|
|
50
50
|
// Variant selection, shared with the Craft themes in frontend-shared the same
|
|
51
|
-
// way the cart and audio player are
|
|
51
|
+
// way the cart and audio player are: so a code site and a Craft site cannot
|
|
52
52
|
// disagree about which version a buyer picked.
|
|
53
53
|
export { useVariantSelection } from "./ui/headless/useVariantSelection";
|
|
54
54
|
// Which version a cart line is. Exported from the package root because the
|
|
@@ -66,7 +66,7 @@ export {
|
|
|
66
66
|
export type { AuthActions } from "./contexts/PublicAuthContext";
|
|
67
67
|
export { PUBLIC_REFRESH_TOKEN_KEY, APP_REFRESH_TOKEN_KEY } from "./client/tokenStorage";
|
|
68
68
|
export { safeRedirectPath } from "./utils/safeRedirect";
|
|
69
|
-
// Ticket-confirmation copy decision
|
|
69
|
+
// Ticket-confirmation copy decision: shared so the Forge block and the client
|
|
70
70
|
// app's finalise page can never tell a refunded buyer two different stories.
|
|
71
71
|
export {
|
|
72
72
|
getTicketOrderOutcome,
|
|
@@ -89,7 +89,7 @@ export {
|
|
|
89
89
|
} from "./contexts/AppAuthContext";
|
|
90
90
|
export { useAppAdminGuard, type AppAdminGuardResult } from "./contexts/useAppAdminGuard";
|
|
91
91
|
|
|
92
|
-
// Data hooks (React Query)
|
|
92
|
+
// Data hooks (React Query): Phase 3
|
|
93
93
|
export * from "./data/queries/useProducts";
|
|
94
94
|
export * from "./data/queries/useMembership";
|
|
95
95
|
export * from "./data/queries/useCourses";
|
|
@@ -151,13 +151,25 @@ export * from "./data/queries/useCallChat";
|
|
|
151
151
|
export * from "./data/queries/useCallBreakouts";
|
|
152
152
|
export * from "./data/queries/useAnalytics";
|
|
153
153
|
export * from "./data/queries/useCommunity";
|
|
154
|
+
// Communities: the list, one community, the viewer's state in it, invites,
|
|
155
|
+
// upgrades and the fan's memberships across communities.
|
|
156
|
+
export * from "./data/queries/useCommunities";
|
|
157
|
+
// The provider every member-content hook reads its community from.
|
|
158
|
+
export {
|
|
159
|
+
CommunityProvider,
|
|
160
|
+
useCommunityContext,
|
|
161
|
+
useOptionalCommunityContext,
|
|
162
|
+
MissingCommunityProviderError,
|
|
163
|
+
type CommunityContextValue,
|
|
164
|
+
type CommunityProviderProps,
|
|
165
|
+
} from "./contexts/CommunityContext";
|
|
154
166
|
export * from "./data/queries/useReviews";
|
|
155
167
|
export * from "./data/queries/useDocuments";
|
|
156
168
|
export * from "./data/queries/useNotifications";
|
|
157
169
|
export * from "./data/queries/usePWA";
|
|
158
170
|
export * from "./data/queries/useCertificates";
|
|
159
171
|
|
|
160
|
-
// Content layer (Phase E)
|
|
172
|
+
// Content layer (Phase E): typed/editable/localizable site content
|
|
161
173
|
export * from "./content";
|
|
162
174
|
|
|
163
175
|
// Utils
|
|
@@ -38,4 +38,35 @@ describe("internal page links", () => {
|
|
|
38
38
|
it("carries every page exactly once", () => {
|
|
39
39
|
expect(new Set(INTERNAL_PAGES).size).toBe(INTERNAL_PAGES.length);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it("carries exactly the community pages, and none of the removed single-community ones", () => {
|
|
43
|
+
const community = INTERNAL_PAGES.filter((page) => page.startsWith("communities"));
|
|
44
|
+
expect(community).toEqual([
|
|
45
|
+
"communities",
|
|
46
|
+
"communities/$slug",
|
|
47
|
+
"communities/$slug/home",
|
|
48
|
+
"communities/$slug/home/calendar",
|
|
49
|
+
"communities/$slug/home/chat",
|
|
50
|
+
"communities/$slug/home/collections",
|
|
51
|
+
"communities/$slug/home/collections/$collectionId",
|
|
52
|
+
"communities/$slug/home/leaderboard",
|
|
53
|
+
"communities/$slug/home/notifications",
|
|
54
|
+
"communities/$slug/home/posts",
|
|
55
|
+
"communities/$slug/home/profile/$accountId",
|
|
56
|
+
"communities/$slug/home/spaces",
|
|
57
|
+
"communities/$slug/home/spaces/$spaceId",
|
|
58
|
+
"communities/$slug/home/spaces/posts/$postId",
|
|
59
|
+
]);
|
|
60
|
+
const removed = INTERNAL_PAGES.filter((page) => /^(membership|members|community|chat)(\/|$)/.test(page));
|
|
61
|
+
expect(removed).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("builds community paths with the slug and recognises them", () => {
|
|
65
|
+
expect(internalPath("communities/$slug/home/spaces/posts/$postId", { slug: "fan-club", postId: "p1" })).toBe(
|
|
66
|
+
"/i/communities/fan-club/home/spaces/posts/p1",
|
|
67
|
+
);
|
|
68
|
+
expect(isInternalPage("communities/fan-club/home")).toBe(true);
|
|
69
|
+
expect(isInternalPage("members/spaces")).toBe(false);
|
|
70
|
+
expect(isInternalPage("membership/checkout")).toBe(false);
|
|
71
|
+
});
|
|
41
72
|
});
|
package/src/runtime/pages.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* for TanStack to know about. That is the reason this helper exists, and it is
|
|
8
8
|
* why the build guide forbids writing an `/i/` path by hand.
|
|
9
9
|
*
|
|
10
|
-
* The page list is the CONTRACT between a site and the hosted pages, so it
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The page list is the CONTRACT between a site and the hosted pages, so it
|
|
11
|
+
* normally only grows: removing a page breaks links on sites already published.
|
|
12
|
+
* The one deliberate exception is Communities (2026-09-27). A profile can now
|
|
13
|
+
* run several communities, so the single-community member pages (`membership`,
|
|
14
|
+
* `members/*`, `community/*`, `chat`) were removed with no redirects, because
|
|
15
|
+
* nobody used them yet and there is no one community they could redirect to.
|
|
16
|
+
* Every member page now lives under `communities/$slug`.
|
|
13
17
|
*
|
|
14
18
|
* <Link {...internalLink("store/$slug", { slug: product.slug })}>Buy</Link>
|
|
15
19
|
* navigate(internalLink("checkout"))
|
|
@@ -28,7 +32,6 @@ export const INTERNAL_PAGES = [
|
|
|
28
32
|
"blog",
|
|
29
33
|
"blog-category/$slug",
|
|
30
34
|
"blog/$slug",
|
|
31
|
-
"chat",
|
|
32
35
|
"checkout",
|
|
33
36
|
"checkout/finalise",
|
|
34
37
|
"checkout/resume",
|
|
@@ -37,11 +40,20 @@ export const INTERNAL_PAGES = [
|
|
|
37
40
|
"coaching/$slug/finalise",
|
|
38
41
|
"cohorts/$cohortId",
|
|
39
42
|
"cohorts/$cohortId/finalise",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
43
|
+
"communities",
|
|
44
|
+
"communities/$slug",
|
|
45
|
+
"communities/$slug/home",
|
|
46
|
+
"communities/$slug/home/calendar",
|
|
47
|
+
"communities/$slug/home/chat",
|
|
48
|
+
"communities/$slug/home/collections",
|
|
49
|
+
"communities/$slug/home/collections/$collectionId",
|
|
50
|
+
"communities/$slug/home/leaderboard",
|
|
51
|
+
"communities/$slug/home/notifications",
|
|
52
|
+
"communities/$slug/home/posts",
|
|
53
|
+
"communities/$slug/home/profile/$accountId",
|
|
54
|
+
"communities/$slug/home/spaces",
|
|
55
|
+
"communities/$slug/home/spaces/$spaceId",
|
|
56
|
+
"communities/$slug/home/spaces/posts/$postId",
|
|
45
57
|
"confirm-subscription",
|
|
46
58
|
"course-access/$accessId",
|
|
47
59
|
"courses",
|
|
@@ -65,19 +77,6 @@ export const INTERNAL_PAGES = [
|
|
|
65
77
|
"live",
|
|
66
78
|
"live/$broadcastId",
|
|
67
79
|
"login",
|
|
68
|
-
"members",
|
|
69
|
-
"members/calendar",
|
|
70
|
-
"members/chat",
|
|
71
|
-
"members/collections",
|
|
72
|
-
"members/collections/$slug",
|
|
73
|
-
"members/spaces",
|
|
74
|
-
"members/spaces/$slug",
|
|
75
|
-
"members/spaces/leaderboard",
|
|
76
|
-
"members/spaces/notifications",
|
|
77
|
-
"members/spaces/profile/$accountId",
|
|
78
|
-
"members/spaces/thread/$postId",
|
|
79
|
-
"membership",
|
|
80
|
-
"membership/checkout",
|
|
81
80
|
"payment-link/$paymentLinkId",
|
|
82
81
|
"payment-link/$paymentLinkId/finalise",
|
|
83
82
|
"podcasts",
|
|
@@ -270,7 +270,7 @@ describe("buildWebManifest — name/short_name fallback chain", () => {
|
|
|
270
270
|
describe("buildWebManifest — overrides (shortcuts) merge", () => {
|
|
271
271
|
const overrides: BuildWebManifestOptions["overrides"] = {
|
|
272
272
|
shortcuts: [
|
|
273
|
-
{ name: "Members", short_name: "Members", url: "/i/
|
|
273
|
+
{ name: "Members", short_name: "Members", url: "/i/communities" },
|
|
274
274
|
{ name: "Account", short_name: "Account", url: "/i/account" },
|
|
275
275
|
],
|
|
276
276
|
};
|
|
@@ -168,7 +168,7 @@ describe("forgeServiceWorkerScript — push payload contract (frozen, shared wit
|
|
|
168
168
|
title: "New message",
|
|
169
169
|
body: "hey!",
|
|
170
170
|
type: "chat_message",
|
|
171
|
-
url: "/i/chat
|
|
171
|
+
url: "/i/communities/fan-club/home/chat",
|
|
172
172
|
profileId: "prof-1",
|
|
173
173
|
channelId: "chan-9",
|
|
174
174
|
icon: "https://cdn.tribenest.co/p/nova/icon-192.png",
|
|
@@ -181,7 +181,7 @@ describe("forgeServiceWorkerScript — push payload contract (frozen, shared wit
|
|
|
181
181
|
body: "hey!",
|
|
182
182
|
icon: "https://cdn.tribenest.co/p/nova/icon-192.png",
|
|
183
183
|
tag: "chat-chan-9",
|
|
184
|
-
data: { url: "/i/chat
|
|
184
|
+
data: { url: "/i/communities/fan-club/home/chat", type: "chat_message" },
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
187
|
|
|
@@ -229,9 +229,9 @@ describe("forgeServiceWorkerScript — notificationclick", () => {
|
|
|
229
229
|
it("focuses an existing client matching the deep-link URL", async () => {
|
|
230
230
|
const { handlers, matchAll, openWindow } = loadSw();
|
|
231
231
|
const focus = vi.fn(() => ({}));
|
|
232
|
-
matchAll.mockResolvedValueOnce([{ url: "https://nova.tribenest.co/i/chat
|
|
232
|
+
matchAll.mockResolvedValueOnce([{ url: "https://nova.tribenest.co/i/communities/fan-club/home/chat", focus }]);
|
|
233
233
|
const close = vi.fn();
|
|
234
|
-
const event = fakeEvent({ notification: { close, data: { url: "/i/chat
|
|
234
|
+
const event = fakeEvent({ notification: { close, data: { url: "/i/communities/fan-club/home/chat" } } });
|
|
235
235
|
handlers.notificationclick!(event);
|
|
236
236
|
await event.waited;
|
|
237
237
|
expect(close).toHaveBeenCalledOnce();
|
|
@@ -72,7 +72,7 @@ describe("parseMembershipGateError", () => {
|
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it("matches on CODE, not on status
|
|
75
|
+
it("matches on CODE, not on status: a read refusal is 401/403, a checkout refusal 400", () => {
|
|
76
76
|
// Nothing here reads a status at all; the same payload parses whichever
|
|
77
77
|
// status carried it. A client switching on 401 would render this as a
|
|
78
78
|
// session timeout.
|
|
@@ -113,7 +113,7 @@ describe("joinTierNames", () => {
|
|
|
113
113
|
expect(joinTierNames(["Gold", "Silver", "Bronze"])).toBe("Gold, Silver or Bronze");
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
it("drops blanks
|
|
116
|
+
it("drops blanks: an unresolved tier must not become an empty slot", () => {
|
|
117
117
|
expect(joinTierNames(["", " ", "Gold"])).toBe("Gold");
|
|
118
118
|
expect(joinTierNames([])).toBe("");
|
|
119
119
|
});
|
|
@@ -160,18 +160,18 @@ describe("buildMembershipGateNotice", () => {
|
|
|
160
160
|
expect(notice!.body).toBe("This ticket is for Gold members. Already one? Sign in and it unlocks.");
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
it("offers the TIER to someone already signed in
|
|
163
|
+
it("offers the TIER to someone already signed in: never sign-in again", () => {
|
|
164
164
|
const notice = buildMembershipGateNotice({
|
|
165
165
|
gate: gate({ reason: "membership_required", requiredTierIds: ["gold", "plat"] }),
|
|
166
166
|
tierNames: ["Gold", "Platinum"],
|
|
167
167
|
itemLabel: "This course",
|
|
168
|
-
membershipPath: "/i/
|
|
168
|
+
membershipPath: "/i/communities/fan-club?tier=gold",
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
expect(notice).toMatchObject({
|
|
172
172
|
reason: "membership_required",
|
|
173
173
|
actionLabel: "View Gold or Platinum",
|
|
174
|
-
actionHref: "/i/
|
|
174
|
+
actionHref: "/i/communities/fan-club?tier=gold",
|
|
175
175
|
});
|
|
176
176
|
expect(notice!.body).toBe("This course is for Gold or Platinum members. Your current plan doesn't include it.");
|
|
177
177
|
// Telling a signed-in member to sign in is a loop with no exit.
|
|
@@ -214,7 +214,7 @@ describe("buildMembershipGateNotice", () => {
|
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it("uses default paths when the host names none", () => {
|
|
217
|
-
expect(buildMembershipGateNotice({ gate: gate() })!.actionHref).toBe("/
|
|
217
|
+
expect(buildMembershipGateNotice({ gate: gate() })!.actionHref).toBe("/i/communities");
|
|
218
218
|
expect(buildMembershipGateNotice({ gate: gate({ reason: "sign_in_required" }) })!.actionHref).toBe("/login");
|
|
219
219
|
});
|
|
220
220
|
});
|
|
@@ -8,22 +8,22 @@ import type { MembershipGateReason, PublicMembershipGate } from "../../types/mod
|
|
|
8
8
|
*
|
|
9
9
|
* The gate shipped ENFORCED and UNANNOUNCED. A logged-out buyer saw an ordinary
|
|
10
10
|
* ticket, chose a seat, typed their name and their email twice, and was refused
|
|
11
|
-
* on the last screen with a `MEMBERSHIP_GATE` 400
|
|
11
|
+
* on the last screen with a `MEMBERSHIP_GATE` 400: the restriction was
|
|
12
12
|
* discoverable only by failing it. And the "members save X, sign in" funnel the
|
|
13
13
|
* feature was justified by existed on no surface at all, because nothing drew a
|
|
14
14
|
* way OUT of the refusal.
|
|
15
15
|
*
|
|
16
16
|
* ## Why it is pure, and why it lives in `format/`
|
|
17
17
|
*
|
|
18
|
-
* There are two rendering stacks
|
|
19
|
-
* websites build against
|
|
18
|
+
* There are two rendering stacks (`apps/client` and the Forge SDK that code
|
|
19
|
+
* websites build against), and a members-only badge that says one thing on an
|
|
20
20
|
* artist's PWA and another on their website is the same bug twice. So the words
|
|
21
21
|
* and the resolving ACTION are decided once, here, with no React and no client;
|
|
22
22
|
* each stack owns only how it looks. `apps/client` imports this from
|
|
23
23
|
* `@tribe-nest/forge/ui`, exactly as it already does for the PWYW arithmetic.
|
|
24
24
|
*
|
|
25
25
|
* Pure and total also means the whole mapping is testable without a DOM or a
|
|
26
|
-
* session
|
|
26
|
+
* session, and getting the two reasons backwards is the failure mode that
|
|
27
27
|
* matters most: telling someone who is already signed in to "sign in" is a loop
|
|
28
28
|
* with no exit, and telling a logged-out visitor to "upgrade" asks them to buy a
|
|
29
29
|
* membership they may already hold.
|
|
@@ -94,7 +94,7 @@ export function parseMembershipGateError(error: unknown): MembershipGateRefusal
|
|
|
94
94
|
/**
|
|
95
95
|
* Is this thing refused to the caller looking at it?
|
|
96
96
|
*
|
|
97
|
-
* `null` means "nothing gates it"
|
|
97
|
+
* `null` means "nothing gates it", which is also what every read returns while
|
|
98
98
|
* the `membership_entitlement_gates` switch is off. Treating an absent gate as
|
|
99
99
|
* "gated and permitted" would badge every item on every site the day the field
|
|
100
100
|
* shipped, so absent must read as false here and nowhere else.
|
|
@@ -110,7 +110,7 @@ export type MembershipGateNotice = {
|
|
|
110
110
|
badge: string;
|
|
111
111
|
title: string;
|
|
112
112
|
body: string;
|
|
113
|
-
/** Primary CTA label
|
|
113
|
+
/** Primary CTA label: the way OUT of the refusal, which is the whole point. */
|
|
114
114
|
actionLabel: string;
|
|
115
115
|
/**
|
|
116
116
|
* Where the CTA goes. `null` only when the caller passed no path to send them
|
|
@@ -120,12 +120,16 @@ export type MembershipGateNotice = {
|
|
|
120
120
|
actionHref: string | null;
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
/** Default sign-in path. Overridden per site
|
|
123
|
+
/** Default sign-in path. Overridden per site: code websites mount theirs under `/i`. */
|
|
124
124
|
export const MEMBERSHIP_GATE_LOGIN_PATH = "/login";
|
|
125
|
-
/**
|
|
126
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Default "become a member" path when the tier's community is not known: the
|
|
127
|
+
* communities list. `useMembershipGateNotice` sends the buyer to the About page
|
|
128
|
+
* of the community the required tier belongs to, with that tier preselected.
|
|
129
|
+
*/
|
|
130
|
+
export const MEMBERSHIP_GATE_MEMBERSHIP_PATH = "/i/communities";
|
|
127
131
|
|
|
128
|
-
/** "Gold", "Gold or Platinum", "Gold, Silver or Bronze"
|
|
132
|
+
/** "Gold", "Gold or Platinum", "Gold, Silver or Bronze". ANY one unlocks, hence "or". */
|
|
129
133
|
export function joinTierNames(names: string[]): string {
|
|
130
134
|
const clean = names.map((name) => name.trim()).filter(Boolean);
|
|
131
135
|
if (clean.length === 0) return "";
|
|
@@ -133,7 +137,7 @@ export function joinTierNames(names: string[]): string {
|
|
|
133
137
|
return `${clean.slice(0, -1).join(", ")} or ${clean[clean.length - 1]}`;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
/** `path?redirect=<here
|
|
140
|
+
/** `path?redirect=<here>`: a sign-in that lands back on the thing they wanted. */
|
|
137
141
|
export function membershipSignInHref(loginPath: string, currentPath: string): string {
|
|
138
142
|
const target = safeRedirectPath(currentPath, "");
|
|
139
143
|
return target ? `${loginPath}?redirect=${encodeURIComponent(target)}` : loginPath;
|
|
@@ -142,7 +146,7 @@ export function membershipSignInHref(loginPath: string, currentPath: string): st
|
|
|
142
146
|
export interface BuildMembershipGateNoticeInput {
|
|
143
147
|
/** The announced gate from a listing/detail read. */
|
|
144
148
|
gate?: PublicMembershipGate | null;
|
|
145
|
-
/** A `MEMBERSHIP_GATE` refusal from a failed purchase. Wins over `gate
|
|
149
|
+
/** A `MEMBERSHIP_GATE` refusal from a failed purchase. Wins over `gate`: it is the newer fact. */
|
|
146
150
|
refusal?: MembershipGateRefusal | null;
|
|
147
151
|
/**
|
|
148
152
|
* Names for `requiredTierIds`, already resolved by the caller (both stacks
|
|
@@ -150,7 +154,7 @@ export interface BuildMembershipGateNoticeInput {
|
|
|
150
154
|
* 8f3c-…" is not a sentence anyone can act on.
|
|
151
155
|
*/
|
|
152
156
|
tierNames?: string[];
|
|
153
|
-
/** What the thing IS, in the buyer's words
|
|
157
|
+
/** What the thing IS, in the buyer's words: "This ticket", "This course". */
|
|
154
158
|
itemLabel?: string;
|
|
155
159
|
loginPath?: string;
|
|
156
160
|
membershipPath?: string;
|
|
@@ -185,7 +189,7 @@ export function buildMembershipGateNotice(
|
|
|
185
189
|
// The API's own sentence is deliberately NOT used for this half. It is
|
|
186
190
|
// written for the refusal ("You must be a member to buy this"), and what
|
|
187
191
|
// an anonymous visitor needs first is the possibility that they already
|
|
188
|
-
// are one
|
|
192
|
+
// are one, which is the difference between a dead end and a funnel.
|
|
189
193
|
body: tiers
|
|
190
194
|
? `${item} is for ${tiers} members. Already one? Sign in and it unlocks.`
|
|
191
195
|
: `${item} is for members. Already one? Sign in and it unlocks.`,
|
|
@@ -201,7 +205,7 @@ export function buildMembershipGateNotice(
|
|
|
201
205
|
body: tiers
|
|
202
206
|
? `${item} is for ${tiers} members. Your current plan doesn't include it.`
|
|
203
207
|
: `${item} is for members. Your current plan doesn't include it.`,
|
|
204
|
-
// NOT "sign in"
|
|
208
|
+
// NOT "sign in": they already are, and offering it again is a loop with no
|
|
205
209
|
// exit. The only move that changes anything is joining the tier.
|
|
206
210
|
actionLabel: tiers ? `View ${tiers}` : "View membership",
|
|
207
211
|
actionHref: membershipPath,
|