Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(expo): address review feedback on theming PR
- Export validation internals via _testing so tests import the real
  implementation instead of a stale copy.
- Add comment documenting that theme loading is centralized in
  ClerkExpoModule (other init paths guard with !isInitialized).
  • Loading branch information
chriscanin committed Apr 14, 2026
commit 4786284db8d40217601be2ccf93a1bcea150f1e8
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
}

Clerk.initialize(reactApplicationContext, pubKey)
// Theme loading is centralized here. ClerkViewFactory.configure()
// and ClerkUserProfileActivity.onCreate() only call Clerk.initialize()
// when Clerk is not yet initialized, so by the time they run
// ClerkExpoModule has already set the custom theme.
// Must be set AFTER Clerk.initialize() because initialize()
// resets customTheme to its `theme` parameter (default null).
loadThemeFromAssets()
Comment thread
manovotny marked this conversation as resolved.
Expand Down
1 change: 1 addition & 0 deletions packages/expo/app.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,3 +716,4 @@ const withClerkExpo = (config, props = {}) => {
};

module.exports = withClerkExpo;
module.exports._testing = { validateThemeJson, isPlainObject, VALID_COLOR_KEYS, HEX_COLOR_REGEX };
67 changes: 1 addition & 66 deletions packages/expo/src/__tests__/appPlugin.theme.test.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';

// The plugin is plain CJS — load the validation function by evaluating
// just the relevant pieces. We re-declare the constants and function
// exactly as they appear in app.plugin.js so the test stays in sync.
// ------------------------------------------------------------------

const VALID_COLOR_KEYS = [
'primary',
'background',
'input',
'danger',
'success',
'warning',
'foreground',
'mutedForeground',
'primaryForeground',
'inputForeground',
'neutral',
'border',
'ring',
'muted',
'shadow',
];

const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;

function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function validateThemeJson(theme) {
if (!isPlainObject(theme)) {
throw new Error('Clerk theme: theme JSON must be a plain object');
}

const validateColors = (colors, label) => {
if (!isPlainObject(colors)) {
throw new Error(`Clerk theme: ${label} must be an object`);
}
for (const [key, value] of Object.entries(colors)) {
if (!VALID_COLOR_KEYS.includes(key)) {
console.warn(`⚠️ Clerk theme: unknown color key "${key}" in ${label}, ignoring`);
continue;
}
if (typeof value !== 'string' || !HEX_COLOR_REGEX.test(value)) {
throw new Error(`Clerk theme: invalid hex color for ${label}.${key}: "${value}"`);
}
}
};

if (theme.colors != null) validateColors(theme.colors, 'colors');
if (theme.darkColors != null) validateColors(theme.darkColors, 'darkColors');

if (theme.design != null) {
if (!isPlainObject(theme.design)) {
throw new Error(`Clerk theme: design must be an object`);
}
if (theme.design.fontFamily != null && typeof theme.design.fontFamily !== 'string') {
throw new Error(`Clerk theme: design.fontFamily must be a string`);
}
if (theme.design.borderRadius != null && typeof theme.design.borderRadius !== 'number') {
throw new Error(`Clerk theme: design.borderRadius must be a number`);
}
}
}

// ------------------------------------------------------------------
const { validateThemeJson } = require('../../app.plugin.js')._testing;

describe('validateThemeJson', () => {
beforeEach(() => {
Expand Down