From 1b48d341e01d6d0399ded69939bbbd3b0000266a Mon Sep 17 00:00:00 2001 From: lukasmisiunas Date: Mon, 22 Sep 2025 21:34:43 +0300 Subject: [PATCH 1/3] refactor(pricing-plans): rename requiredPlanIds to accessPlanIds The previous field name 'requiredPlanIds' was misleading as it suggested content was restricted BY those plan IDs. In reality, content in PlanPaywall.RestrictedContent is locked by default and gets UNLOCKED when the member has one of the specified plan IDs. The new name 'accessPlanIds' clearly indicates these IDs grant access to otherwise restricted content, making the API more intuitive. Changes: - Updated PlanPaywallServiceConfig interface - Updated service logic and component examples - Updated API documentation and demo applications - Rebuilt package with new field name --- docs/api/PLAN_PAYWALL_INTERFACE.md | 12 ++++++------ .../src/components/CoursePageComponent.tsx | 2 +- .../src/components/CoursesPageComponent.tsx | 2 +- .../pricing-plans/src/react/PlanPaywall.tsx | 6 +++--- .../src/services/plan-paywall-service.ts | 18 +++++++++--------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/api/PLAN_PAYWALL_INTERFACE.md b/docs/api/PLAN_PAYWALL_INTERFACE.md index 1f9cfb7de..efd438c12 100644 --- a/docs/api/PLAN_PAYWALL_INTERFACE.md +++ b/docs/api/PLAN_PAYWALL_INTERFACE.md @@ -1,6 +1,6 @@ # Plan Paywall Interface -Component that restricts access to its content until if member does not have access to the required plans. +Component that restricts access to its content unless the member has one of the access plans. ## Architecture @@ -15,7 +15,7 @@ The root container that provides plan paywall context to all child components. **Props** ```tsx interface PlanPaywallServiceConfig { - requiredPlanIds: string[]; + accessPlanIds: string[]; memberOrders?: orders.Order[]; } @@ -28,7 +28,7 @@ interface RootProps { **Example** ```tsx // Restrict by specific plan ids - +
Paywalled content
@@ -45,7 +45,7 @@ interface RootProps { // Load member orders externally const { memberOrders } = await loadPlanPaywallServiceConfig(['planId']); - + {/* Plan paywall components */} ``` @@ -96,7 +96,7 @@ interface PaywallProps { ### PlanPaywall.RestrictedContent -Component that displays the restricted content if the member has access to the required plans. +Component that displays the restricted content if the member has one of the access plans. **Props** ```tsx @@ -115,7 +115,7 @@ interface RestrictedContentProps { ### PlanPaywall.Fallback -Component that displays the fallback content if the member does not have access to the required plans. +Component that displays the fallback content if the member does not have any of the access plans. **Props** ```tsx diff --git a/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx b/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx index 99fcb95ca..e0b540335 100644 --- a/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx +++ b/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx @@ -17,7 +17,7 @@ export const CoursePageComponent = (props: CoursePageComponentProps) => { return ( diff --git a/examples/astro-pricing-plans-demo/src/components/CoursesPageComponent.tsx b/examples/astro-pricing-plans-demo/src/components/CoursesPageComponent.tsx index 482f5f638..3b210bf2f 100644 --- a/examples/astro-pricing-plans-demo/src/components/CoursesPageComponent.tsx +++ b/examples/astro-pricing-plans-demo/src/components/CoursesPageComponent.tsx @@ -327,7 +327,7 @@ const CoursesPageComponent: React.FC = () => { diff --git a/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx b/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx index 72e061ffc..3f04477a5 100644 --- a/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx +++ b/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx @@ -22,7 +22,7 @@ interface RootProps { * @component * @example * ```tsx - * + * * * *
Paywalled content
@@ -112,7 +112,7 @@ interface RestrictedContentProps { } /** - * Component that displays the restricted content if the member has access to the required plans. + * Component that displays the restricted content if the member has one of the access plans. * * @component * @example @@ -139,7 +139,7 @@ interface FallbackProps { } /** - * Component that displays the fallback content if the member does not have access to the required plans. + * Component that displays the fallback content if the member does not have any of the access plans. * * @component * @example diff --git a/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts b/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts index 1126e53b6..c40aad46c 100644 --- a/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts +++ b/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts @@ -18,7 +18,7 @@ export const PlanPaywallServiceDefinition = defineService<{ }>('planPaywallService'); export interface PlanPaywallServiceConfig { - requiredPlanIds: string[]; + accessPlanIds: string[]; memberOrders?: orders.Order[]; } @@ -43,7 +43,7 @@ export const PlanPaywallService = const activePlanOrders = memberOrders.filter( (order) => order.status === 'ACTIVE' && - config.requiredPlanIds.includes(order.planId!), + config.accessPlanIds.includes(order.planId!), ); return activePlanOrders.length > 0; } @@ -51,10 +51,10 @@ export const PlanPaywallService = }); if (!config.memberOrders) { - loadMemberOrders(config.requiredPlanIds); + loadMemberOrders(config.accessPlanIds); } - async function loadMemberOrders(requiredPlanIds: string[]) { + async function loadMemberOrders(accessPlanIds: string[]) { try { isLoadingSignal.set(true); errorSignal.set(null); @@ -64,7 +64,7 @@ export const PlanPaywallService = return; } - const memberOrders = await fetchMemberOrders(requiredPlanIds); + const memberOrders = await fetchMemberOrders(accessPlanIds); memberOrdersSignal.set(memberOrders); } catch (error) { errorSignal.set( @@ -106,12 +106,12 @@ function isMemberLoggedIn(): boolean { } export async function loadPlanPaywallServiceConfig( - requiredPlanIds: string[], + accessPlanIds: string[], ): Promise { if (!isMemberLoggedIn()) { - return { memberOrders: [], requiredPlanIds }; + return { memberOrders: [], accessPlanIds }; } - const memberOrders = await fetchMemberOrders(requiredPlanIds); - return { memberOrders: memberOrders ?? [], requiredPlanIds }; + const memberOrders = await fetchMemberOrders(accessPlanIds); + return { memberOrders: memberOrders ?? [], accessPlanIds }; } From bf048a6182c8da6d0f6a2db3f1272125b34aebee Mon Sep 17 00:00:00 2001 From: lukasmisiunas Date: Mon, 22 Sep 2025 21:55:48 +0300 Subject: [PATCH 2/3] Add isLoggedIn flag to PlanPaywall --- docs/api/PLAN_PAYWALL_INTERFACE.md | 17 ++++++++++++- .../pricing-plans/src/react/PlanPaywall.tsx | 25 +++++++++++-------- .../src/react/core/PlanPaywall.tsx | 9 ++++--- .../src/services/plan-paywall-service.ts | 5 ++++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/docs/api/PLAN_PAYWALL_INTERFACE.md b/docs/api/PLAN_PAYWALL_INTERFACE.md index efd438c12..2bbac36cf 100644 --- a/docs/api/PLAN_PAYWALL_INTERFACE.md +++ b/docs/api/PLAN_PAYWALL_INTERFACE.md @@ -61,6 +61,13 @@ interface PaywallProps { children: AsChildChildren | React.ReactNode; loadingState?: React.ReactNode; } + +interface PlanPaywallData { + isLoading: boolean; + error: string | null; + hasAccess: boolean; + isLoggedIn: boolean; +} ``` **Example** @@ -72,7 +79,7 @@ interface PaywallProps { // With asChild - {React.forwardRef(({isLoading, error, hasAccess}, ref) => { + {React.forwardRef(({isLoading, error, hasAccess, isLoggedIn}, ref) => { if (isLoading) { return
Loading...
; } @@ -81,6 +88,10 @@ interface PaywallProps { return
Error: {error.message}
; } + if (!isLoggedIn) { + return
Please log in to access this content
; + } + if (hasAccess) { return
Paywalled content
; } @@ -92,6 +103,10 @@ interface PaywallProps { **Data Attributes** - `data-testid="plan-paywall-paywall"` - Applied to paywall element +- `data-is-loading` - Boolean indicating loading state +- `data-has-error` - Boolean indicating error state +- `data-has-access` - Boolean indicating if user has access +- `data-is-logged-in` - Boolean indicating if user is logged in --- ### PlanPaywall.RestrictedContent diff --git a/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx b/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx index 3f04477a5..a22b65fe6 100644 --- a/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx +++ b/packages/headless-components/pricing-plans/src/react/PlanPaywall.tsx @@ -69,7 +69,7 @@ interface PaywallProps { * * // With asChild * - * {React.forwardRef(({isLoading, error, hasAccess}, ref) => { + * {React.forwardRef(({isLoading, error, hasAccess, isLoggedIn}, ref) => { * if (isLoading) { * return loadingState; * } @@ -78,6 +78,10 @@ interface PaywallProps { * return
Error!
; * } * + * if (!isLoggedIn) { + * return
Please log in to access this content
; + * } + * * if (hasAccess) { * return
Paywalled content
; * } @@ -90,15 +94,16 @@ interface PaywallProps { export const Paywall = ({ asChild, children, loadingState }: PaywallProps) => ( {(paywallData) => ( - +
{paywallData.isLoading ? loadingState : (children as React.ReactNode)}
diff --git a/packages/headless-components/pricing-plans/src/react/core/PlanPaywall.tsx b/packages/headless-components/pricing-plans/src/react/core/PlanPaywall.tsx index 6ca66e8a1..b74cd0dfa 100644 --- a/packages/headless-components/pricing-plans/src/react/core/PlanPaywall.tsx +++ b/packages/headless-components/pricing-plans/src/react/core/PlanPaywall.tsx @@ -29,6 +29,7 @@ export interface PaywallData { isLoading: boolean; error: string | null; hasAccess: boolean; + isLoggedIn: boolean; } interface PaywallProps { @@ -36,12 +37,12 @@ interface PaywallProps { } export function Paywall({ children }: PaywallProps) { - const { isLoadingSignal, errorSignal, hasAccessSignal } = useService( - PlanPaywallServiceDefinition, - ); + const { isLoadingSignal, errorSignal, hasAccessSignal, isLoggedInSignal } = + useService(PlanPaywallServiceDefinition); const isLoading = isLoadingSignal.get(); const error = errorSignal.get(); const hasAccess = hasAccessSignal.get(); + const isLoggedIn = isLoggedInSignal.get(); - return children({ isLoading, error, hasAccess }); + return children({ isLoading, error, hasAccess, isLoggedIn }); } diff --git a/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts b/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts index c40aad46c..3d88e848d 100644 --- a/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts +++ b/packages/headless-components/pricing-plans/src/services/plan-paywall-service.ts @@ -15,6 +15,7 @@ export const PlanPaywallServiceDefinition = defineService<{ isLoadingSignal: ReadOnlySignal; errorSignal: ReadOnlySignal; hasAccessSignal: ReadOnlySignal; + isLoggedInSignal: ReadOnlySignal; }>('planPaywallService'); export interface PlanPaywallServiceConfig { @@ -33,6 +34,8 @@ export const PlanPaywallService = const signalsService = getService(SignalsServiceDefinition); const isLoadingSignal = signalsService.signal(false); const errorSignal = signalsService.signal(null); + const isLoggedInSignal = + signalsService.signal(isMemberLoggedIn()); const memberOrdersSignal = signalsService.signal( config.memberOrders ?? null, ); @@ -59,6 +62,7 @@ export const PlanPaywallService = isLoadingSignal.set(true); errorSignal.set(null); const isLoggedIn = isMemberLoggedIn(); + isLoggedInSignal.set(isLoggedIn); if (!isLoggedIn) { memberOrdersSignal.set(null); return; @@ -81,6 +85,7 @@ export const PlanPaywallService = isLoadingSignal, errorSignal, hasAccessSignal, + isLoggedInSignal, }; }, ); From 1ede2c675f2f8c6e02211cd9cb395943156f7284 Mon Sep 17 00:00:00 2001 From: lukasmisiunas Date: Wed, 24 Sep 2025 19:49:37 +0300 Subject: [PATCH 3/3] Add accessPlanIds and isLoggedIn props --- docs/api/PLAN_PAYWALL_INTERFACE.md | 33 +++++-- .../src/components/CoursePageComponent.tsx | 36 ++++--- .../pricing-plans/package.json | 2 +- .../pricing-plans/src/react/PlanPaywall.tsx | 93 ++++++++++++------- .../src/react/core/PlanPaywall.tsx | 12 ++- .../pricing-plans/src/react/index.ts | 5 + .../src/services/plan-paywall-service.ts | 2 + 7 files changed, 125 insertions(+), 58 deletions(-) diff --git a/docs/api/PLAN_PAYWALL_INTERFACE.md b/docs/api/PLAN_PAYWALL_INTERFACE.md index 2bbac36cf..9f0a3d098 100644 --- a/docs/api/PLAN_PAYWALL_INTERFACE.md +++ b/docs/api/PLAN_PAYWALL_INTERFACE.md @@ -100,13 +100,6 @@ interface PlanPaywallData { })}
``` - -**Data Attributes** -- `data-testid="plan-paywall-paywall"` - Applied to paywall element -- `data-is-loading` - Boolean indicating loading state -- `data-has-error` - Boolean indicating error state -- `data-has-access` - Boolean indicating if user has access -- `data-is-logged-in` - Boolean indicating if user is logged in --- ### PlanPaywall.RestrictedContent @@ -135,14 +128,29 @@ Component that displays the fallback content if the member does not have any of **Props** ```tsx interface FallbackProps { - children: React.ReactNode; + asChild?: boolean; + children: + | AsChildChildren<{ accessPlanIds: string[]; isLoggedIn: boolean }> + | React.ReactNode; } ``` **Example** ```tsx +// Default usage -
You need to buy a plan to access this content
+
Fallback content
+
+ +// With asChild with react component + + {React.forwardRef(({accessPlanIds, isLoggedIn}, ref) => { + if (!isLoggedIn) { + return
Please log in to access this content
; + } + + return
You need to buy one of the following plans to access this content: {accessPlanIds.join(', ')}
; + })}
``` --- @@ -161,6 +169,9 @@ interface ErrorComponentProps { **Example** ```tsx +// Default usage + + // With asChild
There was an error checking member access
@@ -175,3 +186,7 @@ interface ErrorComponentProps { ))}
``` + +**Data Attributes** +- `data-testid="plan-paywall-error-component"` - Applied to error component +--- diff --git a/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx b/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx index e0b540335..df14469dc 100644 --- a/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx +++ b/examples/astro-pricing-plans-demo/src/components/CoursePageComponent.tsx @@ -3,7 +3,10 @@ import { useWixClient } from '../hooks/useWixClient'; import { getLevelBadgeClass } from '../utils/course-utils'; import { PlanCardContent } from './PlanCard'; import type { Course } from '../utils/demo-courses'; -import { PricingPlans } from '@wix/headless-pricing-plans/react'; +import { + PricingPlans, + type PlanPaywallFallbackData, +} from '@wix/headless-pricing-plans/react'; interface CoursePageComponentProps { course: Course; @@ -25,7 +28,16 @@ export const CoursePageComponent = (props: CoursePageComponentProps) => { - + {React.forwardRef( + ({ accessPlanIds, isLoggedIn }, ref) => ( + + ), + )}
@@ -35,11 +47,11 @@ export const CoursePageComponent = (props: CoursePageComponentProps) => { return ; }; -const RestrictedCourseFallback: React.FC = ({ - course, -}) => { - const { getIsLoggedIn, login, logout } = useWixClient(); - const [isLoggedIn] = useState(getIsLoggedIn()); +const RestrictedCourseFallback = React.forwardRef< + HTMLDivElement, + PlanPaywallFallbackData & { course: Course } +>(({ course, isLoggedIn, accessPlanIds }, ref) => { + const { login, logout } = useWixClient(); const authLinkText = isLoggedIn ? 'Logout' : 'Login'; @@ -52,7 +64,10 @@ const RestrictedCourseFallback: React.FC = ({ }; return ( -
+
{/* Navigation */}