Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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