Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7fa15e0
refactor(icon-hero): remove fixed width to respect parent padding
esaugomez31 Mar 30, 2026
b26b9fd
feat(theme): add _warningLight color for status screen backgrounds
esaugomez31 Mar 30, 2026
bc45f2c
feat(i18n): add AccountMigration translation namespace
esaugomez31 Mar 30, 2026
21ddf7b
feat(icons): register clock icon in GaloyIcon
esaugomez31 Mar 30, 2026
945179e
feat(components): add StatusScreenLayout component
esaugomez31 Mar 30, 2026
8c06abd
feat(migration): add MigrationExplainerLayout component
esaugomez31 Mar 30, 2026
f5e3e3d
feat(migration): add custodial to non-custodial explainer screen
esaugomez31 Mar 30, 2026
f6eaaa1
feat(migration): add transferring funds screen
esaugomez31 Mar 30, 2026
eeb2faf
feat(migration): add barrel export
esaugomez31 Mar 30, 2026
dc1879a
feat(migration): add settings entry point and navigation
esaugomez31 Mar 30, 2026
a929f42
feat(migration): navigate to transferring funds when user has balance
esaugomez31 Mar 30, 2026
8e17589
test(settings): add Move to non-custodial settings row test
esaugomez31 Mar 30, 2026
3b29b84
test(components): add StatusScreenLayout tests
esaugomez31 Mar 30, 2026
c021c26
test(migration): add explainer screen tests
esaugomez31 Mar 30, 2026
02fbb26
test(migration): add transferring funds screen tests
esaugomez31 Mar 30, 2026
ad9e744
feat(migration): add migration checkpoint hook with expiration
esaugomez31 Mar 30, 2026
3865a78
feat(migration): save checkpoint on backup method screen
esaugomez31 Mar 30, 2026
c7f4e6a
feat(migration): save checkpoint on backup alerts screen
esaugomez31 Mar 30, 2026
30c8781
feat(migration): clear checkpoint on backup success screen
esaugomez31 Mar 30, 2026
56fff1b
feat(migration): resume from checkpoint in settings entry
esaugomez31 Mar 30, 2026
e7badd4
test(migration): add checkpoint hook tests with expiration
esaugomez31 Mar 30, 2026
852e388
test(migration): add checkpoint mocks to backup success tests
esaugomez31 Mar 30, 2026
2464b12
test(migration): add checkpoint mocks to backup confirm tests
esaugomez31 Mar 30, 2026
4f32095
refactor(migration): remove unnecessary checkpoint imports from screens
esaugomez31 Mar 30, 2026
1b16cd1
style: restore import group spacing in backup confirm screen
esaugomez31 Mar 30, 2026
03a715c
style(status-screen-layout): increase horizontal padding to 40
esaugomez31 Mar 30, 2026
6bed3d5
refactor(migration): use global 48h checkpoint expiration
esaugomez31 Mar 31, 2026
839f3ea
refactor: extract openExternalUrl helper
esaugomez31 Apr 8, 2026
84a76d1
refactor: extract migration checkpoint into pure module
esaugomez31 Apr 8, 2026
813439c
test: add saveCheckpoint assertions for backup screens
esaugomez31 Apr 8, 2026
b4eb014
refactor: extract NumberedStepsList component
esaugomez31 Apr 8, 2026
770ea32
refactor: simplify StatusScreenLayout icon wrapping
esaugomez31 Apr 8, 2026
10a92b9
feat: extend RichText with tag-based parsing
esaugomez31 Apr 8, 2026
02cb1f6
refactor: merge explainer i18n keys with inline link tag
esaugomez31 Apr 8, 2026
baf3f14
fix: add InAppBrowser to Jest transform ignore and fix act warning
esaugomez31 Apr 8, 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
56 changes: 56 additions & 0 deletions __tests__/components/status-screen-layout.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react"
import { Text } from "react-native"
import { render, screen } from "@testing-library/react-native"

import { StatusScreenLayout } from "@app/components/status-screen-layout"
import { ContextForScreen } from "../screens/helper"

jest.mock("@app/components/atomic/galoy-icon", () => ({
GaloyIcon: ({ name }: { name: string }) => {
const { Text } = jest.requireActual("react-native")
return <Text testID="galoy-icon">{name}</Text>
},
}))

describe("StatusScreenLayout", () => {
it("renders icon and children", () => {
render(
<ContextForScreen>
<StatusScreenLayout icon="clock" iconBackgroundColor="#FFF9E5">
<Text>Loading message</Text>
</StatusScreenLayout>
</ContextForScreen>,
)

expect(screen.getByText("Loading message")).toBeTruthy()
expect(screen.getByTestId("galoy-icon")).toBeTruthy()
})

it("renders without icon background when not provided", () => {
render(
<ContextForScreen>
<StatusScreenLayout icon="payment-success">
<Text>Success</Text>
</StatusScreenLayout>
</ContextForScreen>,
)

expect(screen.getByText("Success")).toBeTruthy()
})

it("renders footer when provided", () => {
render(
<ContextForScreen>
<StatusScreenLayout
icon="clock"
iconBackgroundColor="#FFF9E5"
footer={<Text>Footer</Text>}
>
<Text>Content</Text>
</StatusScreenLayout>
</ContextForScreen>,
)

expect(screen.getByText("Footer")).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { renderHook, act, waitFor } from "@testing-library/react-native"

import {
useMigrationCheckpoint,
MigrationCheckpoint,
} from "@app/screens/account-migration/hooks"

const mockLoadCheckpoint = jest.fn()
const mockSaveCheckpointToStorage = jest.fn()
const mockClearCheckpointFromStorage = jest.fn()

jest.mock("@app/screens/account-migration/utils/migration-checkpoint-storage", () => ({
...jest.requireActual(
"@app/screens/account-migration/utils/migration-checkpoint-storage",
),
loadCheckpoint: (...args: readonly unknown[]) => mockLoadCheckpoint(...args),
saveCheckpointToStorage: (...args: readonly unknown[]) =>
mockSaveCheckpointToStorage(...args),
clearCheckpointFromStorage: (...args: readonly unknown[]) =>
mockClearCheckpointFromStorage(...args),
getStorageKey: (env: string) => `migrationCheckpoint_${env.toLowerCase()}`,
}))

jest.mock("@app/hooks/use-app-config", () => ({
useAppConfig: () => ({
appConfig: { galoyInstance: { name: "Main" } },
}),
}))

describe("useMigrationCheckpoint", () => {
beforeEach(() => {
jest.clearAllMocks()
mockLoadCheckpoint.mockResolvedValue(null)
mockSaveCheckpointToStorage.mockResolvedValue(undefined)
mockClearCheckpointFromStorage.mockResolvedValue(undefined)
})

it("starts with null checkpoint and loading true", async () => {
const { result } = renderHook(() => useMigrationCheckpoint())

expect(result.current.loading).toBe(true)
expect(result.current.checkpoint).toBeNull()

await waitFor(() => expect(result.current.loading).toBe(false))
})

it("loads existing checkpoint from storage", async () => {
mockLoadCheckpoint.mockResolvedValue({
step: MigrationCheckpoint.BackupAlerts,
savedAt: Date.now(),
})

const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.checkpoint).toBe(MigrationCheckpoint.BackupAlerts)
})

it("sets loading false when no checkpoint found", async () => {
const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.checkpoint).toBeNull()
})

it("saves checkpoint to storage", async () => {
const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result.current.loading).toBe(false))

act(() => {
result.current.saveCheckpoint(MigrationCheckpoint.BackupMethod)
})

expect(result.current.checkpoint).toBe(MigrationCheckpoint.BackupMethod)
expect(mockSaveCheckpointToStorage).toHaveBeenCalledWith(
"migrationCheckpoint_main",
MigrationCheckpoint.BackupMethod,
)
})

it("clears checkpoint from storage", async () => {
mockLoadCheckpoint.mockResolvedValue({
step: MigrationCheckpoint.BackupMethod,
savedAt: Date.now(),
})

const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result.current.loading).toBe(false))

act(() => {
result.current.clearCheckpoint()
})

expect(result.current.checkpoint).toBeNull()
expect(mockClearCheckpointFromStorage).toHaveBeenCalledWith(
"migrationCheckpoint_main",
)
})

it("returns default route when no checkpoint", async () => {
const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result.current.loading).toBe(false))

expect(result.current.getRouteForCheckpoint()).toBe("sparkMigrationExplainer")
})

it("returns correct route for checkpoint", async () => {
mockLoadCheckpoint.mockResolvedValue({
step: MigrationCheckpoint.BackupMethod,
savedAt: Date.now(),
})

const { result } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result.current.loading).toBe(false))

expect(result.current.getRouteForCheckpoint()).toBe("sparkBackupMethodScreen")
})

it("resumes from checkpoint after unmount and remount", async () => {
mockLoadCheckpoint.mockResolvedValue(null)

const { result, unmount } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result.current.loading).toBe(false))

act(() => {
result.current.saveCheckpoint(MigrationCheckpoint.BackupAlerts)
})

expect(result.current.checkpoint).toBe(MigrationCheckpoint.BackupAlerts)
expect(mockSaveCheckpointToStorage).toHaveBeenCalledWith(
"migrationCheckpoint_main",
MigrationCheckpoint.BackupAlerts,
)

unmount()

mockLoadCheckpoint.mockResolvedValue({
step: MigrationCheckpoint.BackupAlerts,
savedAt: Date.now(),
})

const { result: result2 } = renderHook(() => useMigrationCheckpoint())

await waitFor(() => expect(result2.current.loading).toBe(false))

expect(result2.current.checkpoint).toBe(MigrationCheckpoint.BackupAlerts)
expect(result2.current.getRouteForCheckpoint()).toBe("sparkBackupAlertsScreen")
})

it("does not update state after unmount", async () => {
let resolveLoad: (value: null) => void
mockLoadCheckpoint.mockReturnValue(
new Promise((resolve) => {
resolveLoad = resolve
}),
)

const { result, unmount } = renderHook(() => useMigrationCheckpoint())

expect(result.current.loading).toBe(true)
unmount()

await act(async () => {
resolveLoad!(null)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from "react"
import { render, fireEvent } from "@testing-library/react-native"
import { Pressable, Text } from "react-native"

import { MigrationExplainerLayout } from "@app/screens/account-migration/migration-explainer-layout"
import { ContextForScreen } from "../helper"

jest.mock("@app/components/icon-hero", () => ({
IconHero: ({ title }: { title: string }) => {
const { Text: RNText } = jest.requireActual("react-native")
return <RNText>{title}</RNText>
},
}))

jest.mock("@app/components/atomic/galoy-primary-button", () => ({
GaloyPrimaryButton: ({ title, onPress }: { title: string; onPress: () => void }) => (
<Pressable testID="cta-button" onPress={onPress}>
<Text>{title}</Text>
</Pressable>
),
}))

jest.mock("@app/components/screen", () => ({
Screen: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))

describe("MigrationExplainerLayout", () => {
const mockOnCtaPress = jest.fn()

const defaultProps = {
icon: "key-outline" as const,
iconColor: "#999",
title: "Test Title",
steps: [
<Text key="1">Step one</Text>,
<Text key="2">Step two</Text>,
<Text key="3">Step three</Text>,
],
ctaTitle: "Continue",
onCtaPress: mockOnCtaPress,
}

beforeEach(() => {
jest.clearAllMocks()
})

it("renders title", () => {
const { getByText } = render(
<ContextForScreen>
<MigrationExplainerLayout {...defaultProps} />
</ContextForScreen>,
)
expect(getByText("Test Title")).toBeTruthy()
})

it("renders all steps", () => {
const { getByText } = render(
<ContextForScreen>
<MigrationExplainerLayout {...defaultProps} />
</ContextForScreen>,
)
expect(getByText("Step one")).toBeTruthy()
expect(getByText("Step two")).toBeTruthy()
expect(getByText("Step three")).toBeTruthy()
})

it("renders step numbers", () => {
const { getByText } = render(
<ContextForScreen>
<MigrationExplainerLayout {...defaultProps} />
</ContextForScreen>,
)
expect(getByText("1.")).toBeTruthy()
expect(getByText("2.")).toBeTruthy()
expect(getByText("3.")).toBeTruthy()
})

it("renders CTA button with title", () => {
const { getByText } = render(
<ContextForScreen>
<MigrationExplainerLayout {...defaultProps} />
</ContextForScreen>,
)
expect(getByText("Continue")).toBeTruthy()
})

it("calls onCtaPress when button pressed", () => {
const { getByTestId } = render(
<ContextForScreen>
<MigrationExplainerLayout {...defaultProps} />
</ContextForScreen>,
)
fireEvent.press(getByTestId("cta-button"))
expect(mockOnCtaPress).toHaveBeenCalledTimes(1)
})
})
Loading
Loading