-
Notifications
You must be signed in to change notification settings - Fork 3
WIP: chore: park YouVersion brand-font implementation #272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jaredhightower-youversion
wants to merge
4
commits into
main
Choose a base branch
from
feat/youversion-brand-fonts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a9d85bf
feat(ui): update Bible version abbreviation title to match Figma
jaredhightower-youversion 3272c83
feat(core,hooks): add Organizations client and useOrganization hook
jaredhightower-youversion 3e42737
feat(hooks): add useOrganizations multi-org resolver hook
jaredhightower-youversion 31b8476
feat(ui): apply YouVersion brand fonts to BibleVersionPicker to match…
jaredhightower-youversion File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| "@youversion/platform-core": minor | ||
| "@youversion/platform-react-hooks": minor | ||
| "@youversion/platform-react-ui": minor | ||
| --- | ||
|
|
||
| Update the Bible Version picker to match the latest Reader SDK Figma design, adding publisher names and refreshing the abbreviation tile. | ||
|
|
||
| - `@youversion/platform-core`: New `OrganizationsClient` with `getOrganization(organizationId)` for fetching an organization by its UUID (`GET /v1/organizations/{id}`), validated against the existing `OrganizationSchema`. The default design tokens now use the YouVersion brand fonts — `--yv-font-sans` is `'Aktiv Grotesk App'` and `--yv-font-serif` is `'Untitled Serif'`, with Inter / Source Serif 4 retained as graceful fallbacks. This applies SDK-wide (all components, including the Bible reader's serif text). | ||
| - `@youversion/platform-react-hooks`: New `useOrganization(organizationId)` hook (plus `useOrganizationsClient`) following the standard `useApiData` pattern. Fetching is skipped when the id is empty. Also adds `useOrganizations(organizationIds)`, which resolves many organizations at once, deduplicated by id, so a list of versions sharing publishers only fetches each organization once. | ||
| - `@youversion/platform-react-ui`: The `BibleVersionPicker` now uses the YouVersion brand fonts to match Figma — labels, headings, version titles, and the publisher name render in Aktiv Grotesk App (`--font-aktiv`), and the abbreviation tile renders in Untitled Serif Bold (new `--font-untitled-serif` token, CDN `@font-face`, falling back to Source Serif 4). `BibleVersionPicker` now renders the publisher name above the version title for versions that have an `organization_id` (rows without an associated organization render the title only), and recently used versions persist `organization_id` so they display the publisher too. Publisher names are resolved once at the list level via `useOrganizations` instead of per row, avoiding N+1 requests when many versions share a publisher. The `VersionAbbreviationIcon` tile now renders as a 64px square with a 6px radius, warm-neutral (`secondary`) fill, themed border, and serif typography using the foreground text color; recent-version and all-version rows share the same tile styling, and long or trailing-digit abbreviations (e.g. `NASB1995` → `NASB` / `1995`) stay readable without overflowing. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { Organization } from '../types'; | ||
|
|
||
| export const mockLockmanOrganization: Organization = { | ||
| id: '798d8fa4-f640-4155-8cfb-fa91d1d8a06c', | ||
| name: 'The Lockman Foundation', | ||
| primary_language: 'en', | ||
| website_url: 'https://www.lockman.org', | ||
| }; | ||
|
|
||
| export const mockBiblicaOrganization: Organization = { | ||
| id: '05a9aa40-37b6-4e34-b9f1-a443fa4b1fff', | ||
| name: 'Biblica', | ||
| primary_language: 'en', | ||
| website_url: 'https://www.biblica.com', | ||
| }; | ||
|
|
||
| export const mockOrganizations: Organization[] = [mockLockmanOrganization, mockBiblicaOrganization]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { describe, it, expect, beforeEach, vi } from 'vitest'; | ||
| import { ApiClient } from '../client'; | ||
| import { OrganizationsClient } from '../organizations'; | ||
| import { OrganizationSchema } from '../schemas'; | ||
|
|
||
| describe('OrganizationsClient', () => { | ||
| let apiClient: ApiClient; | ||
| let organizationsClient: OrganizationsClient; | ||
|
|
||
| beforeEach(() => { | ||
| apiClient = new ApiClient({ | ||
| apiHost: process.env.YVP_API_HOST || '', | ||
| appKey: process.env.YVP_APP_KEY || '', | ||
| installationId: 'test-installation', | ||
| }); | ||
| organizationsClient = new OrganizationsClient(apiClient); | ||
| }); | ||
|
|
||
| describe('getOrganization', () => { | ||
| it('should fetch an organization by ID', async () => { | ||
| const organization = await organizationsClient.getOrganization( | ||
| '798d8fa4-f640-4155-8cfb-fa91d1d8a06c', | ||
| ); | ||
|
|
||
| const { success } = OrganizationSchema.safeParse(organization); | ||
| expect(success).toBe(true); | ||
| expect(organization.id).toBe('798d8fa4-f640-4155-8cfb-fa91d1d8a06c'); | ||
| expect(organization.name).toBe('The Lockman Foundation'); | ||
| }); | ||
|
|
||
| it('should request the organization endpoint with the provided ID', async () => { | ||
| const getSpy = vi.spyOn(apiClient, 'get'); | ||
|
|
||
| await organizationsClient.getOrganization('05a9aa40-37b6-4e34-b9f1-a443fa4b1fff'); | ||
|
|
||
| expect(getSpy).toHaveBeenCalledWith('/v1/organizations/05a9aa40-37b6-4e34-b9f1-a443fa4b1fff'); | ||
| getSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should throw an error for invalid organization ID', async () => { | ||
| await expect(organizationsClient.getOrganization('not-a-uuid')).rejects.toThrow( | ||
| 'Organization ID must be a valid UUID', | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { z } from 'zod'; | ||
| import type { ApiClient } from './client'; | ||
| import { OrganizationSchema } from './schemas'; | ||
| import type { Organization } from './types'; | ||
|
|
||
| /** Client for interacting with Organization API endpoints. */ | ||
| export class OrganizationsClient { | ||
| private client: ApiClient; | ||
|
|
||
| private static readonly organizationIdSchema = z | ||
| .string() | ||
| .trim() | ||
| .uuid('Organization ID must be a valid UUID'); | ||
|
|
||
| /** Creates a new OrganizationsClient instance. */ | ||
| constructor(client: ApiClient) { | ||
| this.client = client; | ||
| } | ||
|
|
||
| /** | ||
| * Fetches an organization by its ID. | ||
| * @param organizationId The organization UUID. | ||
| * @returns The requested Organization object. | ||
| */ | ||
| async getOrganization(organizationId: string): Promise<Organization> { | ||
| const parsedOrganizationId = OrganizationsClient.organizationIdSchema.parse(organizationId); | ||
| const organization = await this.client.get<Organization>( | ||
| `/v1/organizations/${parsedOrganizationId}`, | ||
| ); | ||
|
|
||
| return OrganizationSchema.parse(organization); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { renderHook, waitFor, act } from '@testing-library/react'; | ||
| import { describe, expect, vi, beforeEach, it } from 'vitest'; | ||
| import { useOrganization } from './useOrganization'; | ||
| import { type Organization, type OrganizationsClient } from '@youversion/platform-core'; | ||
| import { useOrganizationsClient } from './useOrganizationsClient'; | ||
| import { createYVWrapper } from './test/utils'; | ||
|
|
||
| vi.mock('./useOrganizationsClient'); | ||
|
|
||
| describe('useOrganization', () => { | ||
| const mockGetOrganization = vi.fn(); | ||
|
|
||
| const mockOrganization: Organization = { | ||
| id: '798d8fa4-f640-4155-8cfb-fa91d1d8a06c', | ||
| name: 'The Lockman Foundation', | ||
| primary_language: 'en', | ||
| website_url: 'https://www.lockman.org', | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| mockGetOrganization.mockResolvedValue(mockOrganization); | ||
|
|
||
| const mockClient: Partial<OrganizationsClient> = { getOrganization: mockGetOrganization }; | ||
| vi.mocked(useOrganizationsClient).mockReturnValue(mockClient as OrganizationsClient); | ||
| }); | ||
|
|
||
| describe('fetching organization', () => { | ||
| it('should fetch organization by ID', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const { result } = renderHook(() => useOrganization('798d8fa4-f640-4155-8cfb-fa91d1d8a06c'), { | ||
| wrapper, | ||
| }); | ||
|
|
||
| expect(result.current.loading).toBe(true); | ||
| expect(result.current.organization).toBe(null); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.loading).toBe(false); | ||
| }); | ||
|
|
||
| expect.soft(mockGetOrganization).toHaveBeenCalledWith('798d8fa4-f640-4155-8cfb-fa91d1d8a06c'); | ||
| expect.soft(result.current.organization).toEqual(mockOrganization); | ||
| }); | ||
|
|
||
| it('should refetch when organizationId changes', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const { rerender } = renderHook(({ organizationId }) => useOrganization(organizationId), { | ||
| wrapper, | ||
| initialProps: { organizationId: '798d8fa4-f640-4155-8cfb-fa91d1d8a06c' }, | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockGetOrganization).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| rerender({ organizationId: '05a9aa40-37b6-4e34-b9f1-a443fa4b1fff' }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockGetOrganization).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| expect(mockGetOrganization).toHaveBeenNthCalledWith( | ||
| 2, | ||
| '05a9aa40-37b6-4e34-b9f1-a443fa4b1fff', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('enabled option', () => { | ||
| it('should not fetch when enabled is false', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const { result } = renderHook( | ||
| () => useOrganization('798d8fa4-f640-4155-8cfb-fa91d1d8a06c', { enabled: false }), | ||
| { wrapper }, | ||
| ); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.loading).toBe(false); | ||
| }); | ||
|
|
||
| expect.soft(mockGetOrganization).not.toHaveBeenCalled(); | ||
| expect.soft(result.current.organization).toBe(null); | ||
| }); | ||
|
|
||
| it('should not fetch when organizationId is empty', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const { result } = renderHook(() => useOrganization(''), { wrapper }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.loading).toBe(false); | ||
| }); | ||
|
|
||
| expect.soft(mockGetOrganization).not.toHaveBeenCalled(); | ||
| expect.soft(result.current.organization).toBe(null); | ||
| }); | ||
| }); | ||
|
|
||
| describe('error handling', () => { | ||
| it('should handle fetch errors', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const error = new Error('Failed to fetch organization'); | ||
| mockGetOrganization.mockRejectedValueOnce(error); | ||
|
|
||
| const { result } = renderHook(() => useOrganization('798d8fa4-f640-4155-8cfb-fa91d1d8a06c'), { | ||
| wrapper, | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.loading).toBe(false); | ||
| }); | ||
|
|
||
| expect.soft(result.current.error).toEqual(error); | ||
| expect.soft(result.current.organization).toBe(null); | ||
| }); | ||
| }); | ||
|
|
||
| describe('manual refetch', () => { | ||
| it('should support manual refetch', async () => { | ||
| const wrapper = createYVWrapper(); | ||
| const { result } = renderHook(() => useOrganization('798d8fa4-f640-4155-8cfb-fa91d1d8a06c'), { | ||
| wrapper, | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(result.current.loading).toBe(false); | ||
| }); | ||
|
|
||
| expect.soft(mockGetOrganization).toHaveBeenCalledTimes(1); | ||
|
|
||
| act(() => { | ||
| result.current.refetch(); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockGetOrganization).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 'use client'; | ||
|
|
||
| import { useApiData, type UseApiDataOptions } from './useApiData'; | ||
| import { type Organization } from '@youversion/platform-core'; | ||
| import { useOrganizationsClient } from './useOrganizationsClient'; | ||
|
|
||
| export function useOrganization( | ||
| organizationId: string, | ||
| apiOptions?: UseApiDataOptions, | ||
| ): { | ||
| organization: Organization | null; | ||
| loading: boolean; | ||
| error: Error | null; | ||
| refetch: () => void; | ||
| } { | ||
| const organizationsClient = useOrganizationsClient(); | ||
| const enabled = apiOptions?.enabled !== false && organizationId.trim().length > 0; | ||
|
|
||
| const { data, loading, error, refetch } = useApiData<Organization>( | ||
| () => organizationsClient.getOrganization(organizationId), | ||
| [organizationsClient, organizationId], | ||
| { | ||
| enabled, | ||
| }, | ||
| ); | ||
|
|
||
| return { | ||
| organization: data, | ||
| loading, | ||
| error, | ||
| refetch, | ||
| }; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changeset describes brand fonts (
--yv-font-sans: 'Aktiv Grotesk App',--yv-font-serif: 'Untitled Serif') as part of the release. Since the PR is explicitly blocked pending licensing, this entry will need to be rewritten before the branch can be merged — otherwise npm consumers will see documentation for fonts they legally cannot use via SDK distribution. The changeset should either be split (font-only vs. non-font work) or have the brand-font references stripped out.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!