diff --git a/packages/headless-components/ecom/src/services/checkout-service.ts b/packages/headless-components/ecom/src/services/checkout-service.ts index 71f128798..183f8ce6e 100644 --- a/packages/headless-components/ecom/src/services/checkout-service.ts +++ b/packages/headless-components/ecom/src/services/checkout-service.ts @@ -9,6 +9,7 @@ import { redirects } from '@wix/redirects'; export { ChannelType } from '@wix/auto_sdk_ecom_checkout'; export type LineItem = checkout.LineItem; +export type CheckoutInfo = checkout.Checkout; /** * API interface for the Checkout service @@ -17,7 +18,10 @@ export interface CheckoutServiceAPI { isLoading: Signal; error: Signal; - createCheckout: (lineItems: LineItem[]) => Promise; + createCheckout: ( + lineItems: LineItem[], + checkoutInfo?: CheckoutInfo, + ) => Promise; } export const CheckoutServiceDefinition = @@ -40,7 +44,10 @@ export const CheckoutService = const isLoading: Signal = signalsService.signal(false); const error: Signal = signalsService.signal(null as any); - const createCheckout = async (lineItems: LineItem[]) => { + const createCheckout = async ( + lineItems: LineItem[], + checkoutInfo?: CheckoutInfo, + ) => { try { isLoading.set(true); error.set(null); @@ -48,6 +55,7 @@ export const CheckoutService = const checkoutResult = await checkout.createCheckout({ lineItems, channelType: config.channelType || checkout.ChannelType.WEB, + checkoutInfo, }); if (!checkoutResult._id) { diff --git a/packages/headless-components/pricing-plans/package.json b/packages/headless-components/pricing-plans/package.json index 7afab086f..b3619fed8 100644 --- a/packages/headless-components/pricing-plans/package.json +++ b/packages/headless-components/pricing-plans/package.json @@ -3,7 +3,7 @@ "version": "0.0.8", "type": "module", "scripts": { - "prebuild": "cd ../utils && yarn build && cd ../components && yarn build && cd ../media && yarn build", + "prebuild": "cd ../utils && yarn build && cd ../components && yarn build && cd ../media && yarn build && cd ../ecom && yarn build", "build": "npm run build:esm && npm run build:cjs", "build:esm": "tsc -p tsconfig.json", "build:cjs": "tsc -p tsconfig.cjs.json", diff --git a/packages/headless-components/pricing-plans/src/react/Plan.tsx b/packages/headless-components/pricing-plans/src/react/Plan.tsx index cda4aa95b..704cc2831 100644 --- a/packages/headless-components/pricing-plans/src/react/Plan.tsx +++ b/packages/headless-components/pricing-plans/src/react/Plan.tsx @@ -1,11 +1,7 @@ import React from 'react'; export { WixMediaImage } from '@wix/headless-media/react'; import { AsChildChildren, AsChildSlot } from '@wix/headless-utils/react'; -import { - PlanServiceConfig, - PlanServiceDefinition, - PRICING_PLANS_APP_ID, -} from '../services/index.js'; +import { PlanServiceConfig } from '../services/index.js'; import { Root as CoreRoot, Plan as CorePlan, @@ -35,10 +31,10 @@ import { PerkItemContext, PerkItemData, PerkItem as CorePerkItem, + ActionBuyNow as CoreActionBuyNow, + ActionBuyNowRenderProps, } from './core/Plan.js'; import { WixMediaImage } from '@wix/headless-media/react'; -import { Commerce } from '@wix/headless-ecom/react'; -import { useService } from '@wix/services-manager-react'; enum PlanTestId { Plan = 'plan-plan', @@ -760,7 +756,17 @@ export const PerkItem = React.forwardRef( ), ); -type ActionBuyNowProps = Omit; +export type PlanActionBuyNowRenderProps = ActionBuyNowRenderProps; + +// TODO: Check if docs are still correct +interface ActionBuyNowProps { + asChild?: boolean; + children?: AsChildChildren; + className?: string; + label?: string; + disabled?: boolean; + loadingState?: React.ReactNode; +} /** * Initiates the plan purchase flow. @@ -787,31 +793,28 @@ type ActionBuyNowProps = Omit; * ``` */ const ActionBuyNow = React.forwardRef( - (props, ref) => { - const { planSignal } = useService(PlanServiceDefinition); - + ({ asChild, children, className, label, disabled, loadingState }, ref) => { return ( - + + {(actionBuyNowRenderProps) => ( + + + + )} + ); }, ); diff --git a/packages/headless-components/pricing-plans/src/react/core/Plan.tsx b/packages/headless-components/pricing-plans/src/react/core/Plan.tsx index 36d400fab..d0c56854d 100644 --- a/packages/headless-components/pricing-plans/src/react/core/Plan.tsx +++ b/packages/headless-components/pricing-plans/src/react/core/Plan.tsx @@ -15,6 +15,10 @@ import { CheckoutService, CheckoutServiceDefinition, } from '@wix/headless-ecom/services'; +import { + PlanCheckoutService, + PlanCheckoutServiceDefinition, +} from '../../services/plan-checkout-service.js'; interface RootProps { planServiceConfig: PlanServiceConfig; @@ -38,7 +42,8 @@ export function Root({ planServiceConfig, children }: RootProps) { channelType: ChannelType.WEB, // TODO: Perhaps we can add postFlowUrl? // postFlowUrl: '' - })} + }) + .addService(PlanCheckoutServiceDefinition, PlanCheckoutService)} > {children} @@ -340,3 +345,36 @@ export function PerkItem({ children }: PerkItemProps) { return children({ perk: perkItem.perk }); } + +export interface ActionBuyNowRenderProps { + isLoading: boolean; + error: string | null; + goToPlanCheckout: () => Promise; +} + +interface ActionBuyNowProps { + children: (props: ActionBuyNowRenderProps) => React.ReactNode; +} + +export function ActionBuyNow({ children }: ActionBuyNowProps) { + const { planSignal } = useService(PlanServiceDefinition); + const { + isLoadingSignal, + errorSignal, + goToPlanCheckout: _goToPlanCheckout, + } = useService(PlanCheckoutServiceDefinition); + const plan = planSignal.get(); + + if (!plan) { + return null; + } + + const isLoading = isLoadingSignal.get(); + const error = errorSignal.get(); + + return children({ + isLoading, + error, + goToPlanCheckout: () => _goToPlanCheckout(plan), + }); +} diff --git a/packages/headless-components/pricing-plans/src/react/index.ts b/packages/headless-components/pricing-plans/src/react/index.ts index 235cf743b..84ae46f50 100644 --- a/packages/headless-components/pricing-plans/src/react/index.ts +++ b/packages/headless-components/pricing-plans/src/react/index.ts @@ -7,6 +7,7 @@ export type { PlanDurationData, PlanPerksData, PlanPerkItemData, + PlanActionBuyNowRenderProps, } from './Plan.js'; export const PricingPlans = { diff --git a/packages/headless-components/pricing-plans/src/services/plan-checkout-service.ts b/packages/headless-components/pricing-plans/src/services/plan-checkout-service.ts new file mode 100644 index 000000000..afc3f62e3 --- /dev/null +++ b/packages/headless-components/pricing-plans/src/services/plan-checkout-service.ts @@ -0,0 +1,82 @@ +import { httpClient } from '@wix/essentials'; +import { CheckoutServiceDefinition } from '@wix/headless-ecom/services'; +import { defineService, implementService } from '@wix/services-definitions'; +import { + ReadOnlySignal, + SignalsServiceDefinition, +} from '@wix/services-definitions/core-services/signals'; +import { PlanWithEnhancedData, PRICING_PLANS_APP_ID } from './plan-service.js'; + +export const PlanCheckoutServiceDefinition = defineService<{ + isLoadingSignal: ReadOnlySignal; + errorSignal: ReadOnlySignal; + goToPlanCheckout: (plan: PlanWithEnhancedData) => Promise; +}>('planCheckoutService'); + +export const PlanCheckoutService = implementService.withConfig()( + PlanCheckoutServiceDefinition, + ({ getService }) => { + const signalsService = getService(SignalsServiceDefinition); + const ecomCheckoutService = getService(CheckoutServiceDefinition); + + const isLoadingSignal = signalsService.signal(false); + const errorSignal = signalsService.signal(null); + + return { + isLoadingSignal, + errorSignal, + goToPlanCheckout: async (plan: PlanWithEnhancedData) => { + isLoadingSignal.set(true); + errorSignal.set(null); + try { + const address = await fetchAddress().catch(() => null); + await ecomCheckoutService.createCheckout( + [ + { + quantity: 1, + catalogReference: { + appId: PRICING_PLANS_APP_ID, + catalogItemId: plan._id!, + options: { + type: 'PLAN', + planOptions: { + pricingVariantId: + plan.enhancedData.price.pricingVariantId, + }, + }, + }, + }, + ], + { billingInfo: address ? { address } : undefined }, + ); + } catch (error) { + errorSignal.set( + error instanceof Error + ? error.message + : 'Failed to go to plan checkout', + ); + } finally { + isLoadingSignal.set(false); + } + }, + }; + }, +); + +type AddressResponse = { + country?: string; + subdivision?: string; + city?: string; +}; + +async function fetchAddress(): Promise { + try { + const response = await httpClient.fetchWithAuth( + '/_api/pricing-plans-ecom/address', + ); + return response.json(); + } catch (error) { + console.error('Error fetching address:', error); + throw error; + } +} diff --git a/packages/headless-components/pricing-plans/src/services/plan-service.ts b/packages/headless-components/pricing-plans/src/services/plan-service.ts index b13824c74..4b17d3272 100644 --- a/packages/headless-components/pricing-plans/src/services/plan-service.ts +++ b/packages/headless-components/pricing-plans/src/services/plan-service.ts @@ -4,6 +4,11 @@ import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals'; import { plansV3 } from '@wix/pricing-plans'; +import { + CheckoutServiceDefinition, + LineItem, +} from '@wix/headless-ecom/services'; +import { httpClient } from '@wix/essentials'; type ValidPeriod = Exclude; @@ -42,6 +47,7 @@ export const PlanServiceDefinition = defineService<{ planSignal: ReadOnlySignal; isLoadingSignal: ReadOnlySignal; errorSignal: ReadOnlySignal; + goToPlanCheckout: (plan: PlanWithEnhancedData) => Promise; }>('planService'); export type PlanServiceConfig = @@ -52,6 +58,7 @@ export const PlanService = implementService.withConfig()( PlanServiceDefinition, ({ getService, config }) => { const signalsService = getService(SignalsServiceDefinition); + const checkoutService = getService(CheckoutServiceDefinition); const isLoadingSignal = signalsService.signal(false); const errorSignal = signalsService.signal(null); const configHasPlan = 'plan' in config; @@ -78,10 +85,20 @@ export const PlanService = implementService.withConfig()( } } + async function goToPlanCheckout(plan: PlanWithEnhancedData): Promise { + const address = await fetchAddress().catch(() => null); + const lineItem = buildPlanLineItem(plan); + // TODO: Decide if we need `externalReference` here + return checkoutService.createCheckout([lineItem], { + billingInfo: address ? { address } : undefined, + }); + } + return { - planSignal: planSignal, - isLoadingSignal: isLoadingSignal, - errorSignal: errorSignal, + planSignal, + isLoadingSignal, + errorSignal, + goToPlanCheckout, }; }, ); @@ -245,3 +262,37 @@ export async function loadPlanServiceConfig( const plan = await fetchAndEnhancePlan(planId); return { plan }; } + +type AddressResponse = { + country?: string; + subdivision?: string; + city?: string; +}; + +async function fetchAddress(): Promise { + try { + const response = await httpClient.fetchWithAuth( + '/_api/pricing-plans-ecom/address', + ); + return response.json(); + } catch (error) { + console.error('Error fetching address:', error); + throw error; + } +} + +function buildPlanLineItem(plan: PlanWithEnhancedData): LineItem { + return { + quantity: 1, + catalogReference: { + appId: PRICING_PLANS_APP_ID, + catalogItemId: plan._id!, + options: { + type: 'PLAN', + planOptions: { + pricingVariantId: plan.enhancedData.price.pricingVariantId, + }, + }, + }, + }; +}