Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/nasty-maps-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/react": patch
---

Add optional appearance to checkout session
57 changes: 57 additions & 0 deletions packages/react/src/components/checkout/utils/case-conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { CSSVariables } from '@/godaddy-provider';

/**
* Convert kebab-case string to camelCase
* @example kebabToCamel('font-sans') // 'fontSans'
*/
export function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}

/**
* Convert camelCase string to kebab-case
* @example camelToKebab('fontSans') // 'font-sans'
*/
export function camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}

/**
* Convert kebab-case CSS variables object to camelCase for GraphQL
* @param variables - Object with kebab-case keys
* @returns Object with camelCase keys
* @example
* convertCSSVariablesToCamelCase({ 'font-sans': 'Arial', 'secondary-background': '#fff' })
* // { fontSans: 'Arial', secondaryBackground: '#fff' }
*/
export function convertCSSVariablesToCamelCase(
variables: CSSVariables
): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(variables)) {
if (value !== undefined) {
result[kebabToCamel(key)] = value;
}
}
return result;
}

/**
* Convert camelCase object keys to kebab-case (for GraphQL response to CSS variables)
* @param obj - Object with camelCase keys
* @returns Object with kebab-case keys typed as CSSVariables
* @example
* convertCamelCaseToKebabCase({ fontSans: 'Arial', secondaryBackground: '#fff' })
* // { 'font-sans': 'Arial', 'secondary-background': '#fff' }
*/
export function convertCamelCaseToKebabCase(
obj: Record<string, string>
): CSSVariables {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
result[camelToKebab(key)] = value;
}
}
return result as CSSVariables;
}
7 changes: 6 additions & 1 deletion packages/react/src/hooks/use-theme.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import { useEffect } from 'react';
import { useCheckoutContext } from '@/components/checkout/checkout';
// hooks/useTheme.ts
import { useGoDaddyContext } from '@/godaddy-provider';

Expand All @@ -13,7 +14,11 @@ export type Theme = keyof typeof themes;

export function useTheme() {
const { appearance } = useGoDaddyContext();
const theme = appearance?.theme;
const { session } = useCheckoutContext();

// Prioritize session appearance over context appearance
const effectiveAppearance = session?.appearance ?? appearance;
const theme = effectiveAppearance?.theme;

useEffect(() => {
// Remove all theme classes
Expand Down
28 changes: 22 additions & 6 deletions packages/react/src/hooks/use-variables.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';
import { useEffect } from 'react';
import { useCheckoutContext } from '@/components/checkout/checkout';
import { convertCamelCaseToKebabCase } from '@/components/checkout/utils/case-conversion';
// hooks/use-variables.ts
import {
type CSSVariables,
Expand All @@ -9,16 +11,29 @@ import {

/**
* Hook that applies CSS variables from the GoDaddy context to the document
* @param {GoDaddyVariables} [overrideVariables] - Optional variables that override context variables
* Priority: overrideVariables > session.appearance > context.appearance
* @param {GoDaddyVariables} [overrideVariables] - Optional variables that override all other variables
*/
export function useVariables(overrideVariables?: GoDaddyVariables) {
const { appearance } = useGoDaddyContext();
const { session } = useCheckoutContext();

// Get variables from both sources
let sessionVariables: CSSVariables | undefined;
if (session?.appearance?.variables) {
// Session variables come from GraphQL in camelCase, convert to kebab-case
sessionVariables = convertCamelCaseToKebabCase(
session.appearance.variables as Record<string, string>
);
}

// Context variables are already in kebab-case
const contextVariables = appearance?.variables;

useEffect(() => {
if (!contextVariables && !overrideVariables) return;
if (!sessionVariables && !contextVariables && !overrideVariables) return;

// Extract CSS variables from context
// Extract CSS variables from context (lowest priority)
let contextCssVars: CSSVariables | undefined;
if (contextVariables) {
if ('checkout' in contextVariables) {
Expand All @@ -28,7 +43,7 @@ export function useVariables(overrideVariables?: GoDaddyVariables) {
}
}

// Extract CSS variables from overrides
// Extract CSS variables from overrides (highest priority)
let overrideCssVars: CSSVariables | undefined;
if (overrideVariables) {
if ('checkout' in overrideVariables) {
Expand All @@ -38,9 +53,10 @@ export function useVariables(overrideVariables?: GoDaddyVariables) {
}
}

// Merge the variables, with overrides taking precedence
// Merge the variables, with priority: override > session > context
const mergedVars: CSSVariables = {
...contextCssVars,
...sessionVariables,
...overrideCssVars,
};

Expand All @@ -61,5 +77,5 @@ export function useVariables(overrideVariables?: GoDaddyVariables) {
rootStyle.removeProperty(`--gd-${key}`);
}
};
}, [contextVariables, overrideVariables]);
}, [sessionVariables, contextVariables, overrideVariables]);
}
38 changes: 36 additions & 2 deletions packages/react/src/lib/godaddy/godaddy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use server';

import { convertCSSVariablesToCamelCase } from '@/components/checkout/utils/case-conversion';
import type { CSSVariables, GoDaddyAppearance } from '@/godaddy-provider';
import type { ResultOf } from '@/gql.tada';
import { graphqlRequestWithErrors } from '@/lib/graphql-with-errors';
import type {
Expand Down Expand Up @@ -38,12 +40,20 @@ import {
DraftOrderTaxesQuery,
} from './queries';

// Type for createCheckoutSession input with kebab-case appearance
export type CreateCheckoutSessionInputWithKebabCase = Omit<
CheckoutSessionInput['input'],
'appearance'
> & {
appearance?: GoDaddyAppearance;
};

function getHostByEnvironment(): string {
return `https://checkout.commerce.${process.env.GODADDY_HOST || process.env.NEXT_PUBLIC_GODADDY_HOST || 'api.godaddy.com'}`;
}

export async function createCheckoutSession(
input: CheckoutSessionInput['input'],
input: CreateCheckoutSessionInputWithKebabCase,
{ accessToken }: { accessToken: string }
): Promise<
ResultOf<typeof CreateCheckoutSessionMutation>['createCheckoutSession']
Expand All @@ -52,13 +62,37 @@ export async function createCheckoutSession(
throw new Error('No public access token provided');
}

// Convert appearance variables from kebab-case to camelCase for GraphQL
let convertedVariables: Record<string, string> | undefined;
if (input.appearance?.variables) {
const variables = input.appearance.variables;
// Check if variables is nested under 'checkout' or is direct CSSVariables
if ('checkout' in variables) {
convertedVariables = convertCSSVariablesToCamelCase(variables.checkout);
} else {
convertedVariables = convertCSSVariablesToCamelCase(variables);
}
}

// Exclude appearance from input and add it back with converted variables
const { appearance, ...restInput } = input;
const graphqlInput: CheckoutSessionInput['input'] = {
...restInput,
...(appearance && {
appearance: {
theme: appearance.theme,
...(convertedVariables && { variables: convertedVariables }),
},
}),
};

const GODADDY_HOST = getHostByEnvironment();
const response = await graphqlRequestWithErrors<
ResultOf<typeof CreateCheckoutSessionMutation>
>(
GODADDY_HOST,
CreateCheckoutSessionMutation,
{ input },
{ input: graphqlInput },
{ Authorization: `Bearer ${accessToken}` }
);

Expand Down
Loading