Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4d0f238
feat(selling-plans): integrate selling plans API and dropdown component
hhuang2-godaddy Mar 4, 2026
c1ebb52
Fix local pickup time offset (#1310)
pbennett1-godaddy Mar 4, 2026
389be9f
Version Packages (#1312)
github-actions[bot] Mar 4, 2026
cfd1b89
refactor(selling-plans): enhance selling plans fetching and dropdown …
hhuang2-godaddy Mar 10, 2026
f9c14b9
refactor(selling-plans): streamline selling plan price resolution and…
hhuang2-godaddy Mar 13, 2026
b3c0dad
fix(react): convert MercadoPago amount from minor to major units (#1323)
nlaverde-godaddy Mar 23, 2026
60c534e
Version Packages (#1324)
github-actions[bot] Mar 23, 2026
ea2fab6
fix(actions): remove inappropriate comment from getSellingPlans function
hhuang2-godaddy Mar 23, 2026
a717f1c
chore(deps): bump pnpm/action-setup from 4 to 5 (#1328)
dependabot[bot] Mar 25, 2026
5533bcd
chore(workflows): update actions to latest and setup OIDC perms (#1330)
rmarkins-godaddy Mar 25, 2026
42dae23
chore: fix repository url
rmarkins-godaddy Mar 25, 2026
ed3cbef
Add pickupSlotInterval and uncouple leadTime from time slot generatio…
pbennett1-godaddy Mar 26, 2026
23d4e30
add pull rquest permission (#1331)
pbennett1-godaddy Mar 26, 2026
badbae9
Version Packages (#1332)
github-actions[bot] Mar 26, 2026
0fabf67
Upgrade to ESLint 10 (#1297)
olafleur-godaddy Mar 26, 2026
7242cea
Version Packages (#1333)
github-actions[bot] Mar 26, 2026
23def91
CHORE: update next, vitest, and stripe deps (#1334)
pbennett1-godaddy Mar 26, 2026
3d1f9aa
refactor(use-add-to-cart): simplify metafields handling for selling p…
hhuang2-godaddy Mar 27, 2026
7f91a80
refactor(cart): enhance cart line item price calculation and selling …
hhuang2-godaddy Mar 30, 2026
65d52c7
Merge branch 'main' into c2Subscritions_selling_plans_frontend
hhuang2-godaddy Apr 1, 2026
353dc8b
Add support for enAu localization (#1337)
pbennett1-godaddy Apr 2, 2026
4e031cb
Version Packages (#1338)
github-actions[bot] Apr 2, 2026
aa51cd8
Merge branch 'main' into c2Subscritions_selling_plans_frontend
hhuang2-godaddy Apr 2, 2026
85bc6fa
refactor(product-details): introduce extensibility slots for product …
hhuang2-godaddy Apr 2, 2026
4e11454
refactor(product-details): update import paths and introduce new cont…
hhuang2-godaddy Apr 2, 2026
57d6115
add commerce api skill (#1339)
pbennett1-godaddy Apr 3, 2026
91b9129
Version Packages (#1340)
github-actions[bot] Apr 3, 2026
8c7e707
Merge branch 'main' into c2Subscritions_selling_plans_frontend
hhuang2-godaddy Apr 3, 2026
8d3f2c2
Update commerce-api skill to utilize godaddy cli (#1341)
pbennett1-godaddy Apr 4, 2026
52bc89b
Version Packages (#1342)
github-actions[bot] Apr 6, 2026
89efb92
Merge branch 'main' into c2Subscritions_selling_plans_frontend
hhuang2-godaddy Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
concurrency: ${{ github.workflow }}-${{ github.ref }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6 # v6.0.2

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6 # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6 # v6.0.2

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v4 # v4.34.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v4 # v4.34.1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
Expand All @@ -64,4 +64,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v4 # v4.34.1
14 changes: 9 additions & 5 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ on:

concurrency: ${{ github.workflow }}-${{ github.ref }}

permissions:
id-token: write # Required for OIDC
contents: write # Required to push tags and releases
pull-requests: write # Required for changesets to create release PRs

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
uses: actions/checkout@v6 # v6.0.2

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5 # v5.0.0

- name: Setup Node
uses: actions/setup-node@v5
uses: actions/setup-node@v6 # v6.3.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
Expand All @@ -29,10 +34,9 @@ jobs:

- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
uses: changesets/action@v1 # v1.7.0
with:
version: pnpm run version
publish: pnpm publish -r
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_SECRET }}
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Close Stale Issues
uses: actions/stale@v10
uses: actions/stale@v10 # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-pr-message: |
Expand Down
43 changes: 43 additions & 0 deletions examples/nextjs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
# nextjs

## 0.1.33

### Patch Changes

- Updated dependencies [8d3f2c2]
- @godaddy/[email protected]

## 0.1.32

### Patch Changes

- Updated dependencies [57d6115]
- @godaddy/[email protected]

## 0.1.31

### Patch Changes

- Updated dependencies [353dc8b]
- @godaddy/[email protected]
- @godaddy/[email protected]

## 0.1.30

### Patch Changes

- Updated dependencies [ed3cbef]
- @godaddy/[email protected]

## 0.1.29

### Patch Changes

- Updated dependencies [b3c0dad]
- @godaddy/[email protected]

## 0.1.28

### Patch Changes

- Updated dependencies [c1ebb52]
- @godaddy/[email protected]

## 0.1.27

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default async function Home() {
},
paypal: {
processor: 'paypal',
checkoutTypes: ['express', 'standard'],
checkoutTypes: ['standard'],
},
},
operatingHours: {
Expand Down
35 changes: 35 additions & 0 deletions examples/nextjs/app/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
import { createCheckoutSession } from '@godaddy/react/server';
import { redirect } from 'next/navigation';

/**
* Fetches selling plans for given SKU IDs from the external (or local) selling-plans API.
* Only runs when SELLING_PLANS_API_URL is set (e.g. in .env.local). If unset, returns
* empty so no request is made and no 404 is logged.
*/
export async function getSellingPlans(storeId: string, options: any) {
const base = process.env.SELLING_PLANS_API_URL;
if (!base) {
return null;
}
// API shape: GET /api/v1/selling-plans/{storeId}/groups?skuIds=...
const path = `/api/v1/selling-plans/${encodeURIComponent(storeId)}/groups`
const url = new URL(path, base);
for (const id of options.skuIds ?? []) {
url.searchParams.append('skuIds', id);
}
for (const id of options.skuGroupIds ?? []) {
url.searchParams.append('skuGroupIds', id);
}
url.searchParams.append('includes', 'catalogPrices');
url.searchParams.append('includes', 'allocations');
// url.searchParams.append('planStatus', 'ACTIVE'); // uncomment it when push to repo
try {
const res = await fetch(url.toString(), { cache: 'no-store' });
if (!res.ok) {
return null;
}
const data = await res.json();
return data.groups;
} catch {
// Network/socket errors (e.g. selling-plans API not running): fail silently so product page still works
return null;
}
}

export async function checkoutWithOrder(orderId: string) {
const session = await createCheckoutSession(
{
Expand Down
28 changes: 27 additions & 1 deletion examples/nextjs/app/store/product/[productId]/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
import { ProductDetails } from '@godaddy/react';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { SellingPlanDropdown } from '../selling-plan-dropdown';
import { useCart } from '../../layout';

/** Route param productId is the SKU Group id (product id from catalog skuGroups). */
export default function Product({ productId }: { productId: string }) {
const { openCart } = useCart();
const [selectedSellingPlanId, setSelectedSellingPlanId] = useState<string | null>(null);
const [selectedSellingPlan, setSelectedSellingPlan] = useState<any>(null);

const handleSellingPlanChange = (planId: string | null, plan: any) => {
setSelectedSellingPlanId(planId);
setSelectedSellingPlan(plan);
};

return (
<div className='container mx-auto'>
Expand All @@ -17,7 +27,23 @@ export default function Product({ productId }: { productId: string }) {
<ArrowLeft className='h-4 w-4' />
Back to Store
</Link>
<ProductDetails productId={productId} onAddToCartSuccess={openCart} />
<ProductDetails
productId={productId}
onAddToCartSuccess={openCart}
selectedSellingPlanId={selectedSellingPlanId}
selectedSellingPlan={selectedSellingPlan}
targets={{
'product-details.add-to-cart.before': ({ skuId, storeId }) => (
<SellingPlanDropdown
storeId={storeId ?? process.env.NEXT_PUBLIC_GODADDY_STORE_ID ?? ''}
skuId={skuId}
skuGroupId={productId}
selectedPlanId={selectedSellingPlanId}
onSelectionChange={handleSellingPlanChange}
/>
),
}}
/>
</div>
);
}
150 changes: 150 additions & 0 deletions examples/nextjs/app/store/product/selling-plan-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import { useCallback, useEffect, useState } from 'react';
import { getSellingPlans } from '../actions';

/**
* Resolve checkout price for the current SKU from the plan's catalogPrices.
* API shape: plan.catalogPrices[] has { skuId, checkoutPrices: [{ value, currency }] }.
* Picks the entry matching skuId and returns checkoutPrices[0] as { value, currencyCode }.
* Value is in minor units (cents).
*/
function normalizeCheckoutPrice(
plan: any,
skuId: string | null
): { value: number; currencyCode?: string } | undefined {
if (plan.checkoutPrice?.value != null) {
return {
value: Number(plan.checkoutPrice.value),
currencyCode: plan.checkoutPrice.currencyCode ?? plan.checkoutPrice.currency,
};
}
if (skuId && Array.isArray(plan.catalogPrices)) {
const forSku = plan.catalogPrices.find((c: any) => c.skuId === skuId);
const checkout = forSku?.checkoutPrices?.[0];
if (checkout?.value != null) {
return {
value: Number(checkout.value),
currencyCode: checkout.currencyCode ?? checkout.currency,
};
}
}
const first = plan.catalogPrices?.[0];
const checkoutFirst = first?.checkoutPrices?.[0];
if (checkoutFirst?.value != null) {
return {
value: Number(checkoutFirst.value),
currencyCode: checkoutFirst.currencyCode ?? checkoutFirst.currency,
};
}
if (plan.priceAtCheckout?.value != null) {
return {
value: Number(plan.priceAtCheckout.value),
currencyCode: plan.priceAtCheckout.currencyCode ?? plan.priceAtCheckout.currency,
};
}
return undefined;
}

export function SellingPlanDropdown({
storeId,
skuId,
skuGroupId,
selectedPlanId,
onSelectionChange,
}: {
storeId: string;
skuId: string | null;
skuGroupId: string | null;
selectedPlanId: string | null;
onSelectionChange: (planId: string | null, plan: any) => void;
}) {
const [plans, setPlans] = useState<any[]>([]);
const [loading, setLoading] = useState(false);

/**
* Loads selling plans from the selling-plans API for the current skuId/skuGroupId.
*
* Production note: This triggers one API call per product (or per PDP visit). To reduce
* requests, consider a strategy to preload selling groups on the store list page: fetch
* groups for all visible skuIds and skuGroupIds once, pass the result (e.g. via context
* or cache) into this component.
*/
const loadPlans = useCallback(async () => {
if (!storeId || (!skuId && !skuGroupId)) {
setPlans([]);
return;
}
setLoading(true);
try {
const groups: any = await getSellingPlans(storeId, {
skuIds: skuId ? [skuId] : [],
skuGroupIds: skuGroupId ? [skuGroupId] : [],
}) ?? [];
// 1. Max 2 selling groups: one for skuId, one for skuGroupId
// 2. Each group has sellingPlans and allocations
// 3. Each allocation has resourceType (SKU | SKU_GROUP) and resourceId
// 4. Prefer group that matches skuId; else group that matches skuGroupId; else []
const forSku = groups?.find((g: any) =>
(g.allocations ?? []).some(
(a: any) => a.resourceType === 'SKU' && a.resourceId === skuId
)
);
const forSkuGroup = groups?.find((g: any) =>
(g.allocations ?? []).some(
(a: any) =>
a.resourceType === 'SKU_GROUP' && a.resourceId === skuGroupId
)
);
const sellingPlans = (forSku ?? forSkuGroup)?.sellingPlans ?? [];
setPlans(sellingPlans);
if (sellingPlans.length === 0) {
onSelectionChange(null, null);
}
} finally {
setLoading(false);
}
}, [storeId, skuId, skuGroupId, onSelectionChange]);

useEffect(() => {
loadPlans();
}, [loadPlans]);

if (loading || plans.length === 0) {
return null;
}

const value = selectedPlanId ?? '';

return (
<div className='space-y-2'>
<label className='text-sm font-medium text-foreground' htmlFor='selling-plan'>
Subscription
</label>
<select
id='selling-plan'
className='flex h-10 w-full items-center justify-between rounded-md border border-border bg-input px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring'
value={value}
onChange={e => {
const val = e.target.value;
const raw = val ? plans.find(p => p.planId === val) ?? null : null;
const plan = raw
? {
...raw,
checkoutPrice: normalizeCheckoutPrice(raw, skuId),
}
: null;
onSelectionChange(val || null, plan);
}}
>
<option value=''>One-time purchase</option>
{plans.map(p => (
<option key={p.planId} value={p.planId}>
{p.name ?? p.planId}
{p.category ? ` · ${p.category}` : ''}
</option>
))}
</select>
</div>
);
}
4 changes: 4 additions & 0 deletions examples/nextjs/env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ NEXT_PUBLIC_PAYPAL_CLIENT_ID=
# MercadoPago Credentials
NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY=
NEXT_PUBLIC_MERCADOPAGO_COUNTRY=AR

# Selling plans API (external or local in dev)
# In local development set to your local API base URL (e.g. http://localhost:8443)
SELLING_PLANS_API_URL=
Loading