diff --git a/__tests__/components/status-screen-layout.spec.tsx b/__tests__/components/status-screen-layout.spec.tsx
new file mode 100644
index 0000000000..6cc50ea0f7
--- /dev/null
+++ b/__tests__/components/status-screen-layout.spec.tsx
@@ -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 {name}
+ },
+}))
+
+describe("StatusScreenLayout", () => {
+ it("renders icon and children", () => {
+ render(
+
+
+ Loading message
+
+ ,
+ )
+
+ expect(screen.getByText("Loading message")).toBeTruthy()
+ expect(screen.getByTestId("galoy-icon")).toBeTruthy()
+ })
+
+ it("renders without icon background when not provided", () => {
+ render(
+
+
+ Success
+
+ ,
+ )
+
+ expect(screen.getByText("Success")).toBeTruthy()
+ })
+
+ it("renders footer when provided", () => {
+ render(
+
+ Footer}
+ >
+ Content
+
+ ,
+ )
+
+ expect(screen.getByText("Footer")).toBeTruthy()
+ })
+})
diff --git a/__tests__/screens/account-migration/hooks/use-migration-checkpoint.spec.ts b/__tests__/screens/account-migration/hooks/use-migration-checkpoint.spec.ts
new file mode 100644
index 0000000000..d3b4b1ed49
--- /dev/null
+++ b/__tests__/screens/account-migration/hooks/use-migration-checkpoint.spec.ts
@@ -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)
+ })
+ })
+})
diff --git a/__tests__/screens/account-migration/migration-explainer-layout.spec.tsx b/__tests__/screens/account-migration/migration-explainer-layout.spec.tsx
new file mode 100644
index 0000000000..6b6ff3825e
--- /dev/null
+++ b/__tests__/screens/account-migration/migration-explainer-layout.spec.tsx
@@ -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 {title}
+ },
+}))
+
+jest.mock("@app/components/atomic/galoy-primary-button", () => ({
+ GaloyPrimaryButton: ({ title, onPress }: { title: string; onPress: () => void }) => (
+
+ {title}
+
+ ),
+}))
+
+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: [
+ Step one,
+ Step two,
+ Step three,
+ ],
+ ctaTitle: "Continue",
+ onCtaPress: mockOnCtaPress,
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("renders title", () => {
+ const { getByText } = render(
+
+
+ ,
+ )
+ expect(getByText("Test Title")).toBeTruthy()
+ })
+
+ it("renders all steps", () => {
+ const { getByText } = render(
+
+
+ ,
+ )
+ expect(getByText("Step one")).toBeTruthy()
+ expect(getByText("Step two")).toBeTruthy()
+ expect(getByText("Step three")).toBeTruthy()
+ })
+
+ it("renders step numbers", () => {
+ const { getByText } = render(
+
+
+ ,
+ )
+ expect(getByText("1.")).toBeTruthy()
+ expect(getByText("2.")).toBeTruthy()
+ expect(getByText("3.")).toBeTruthy()
+ })
+
+ it("renders CTA button with title", () => {
+ const { getByText } = render(
+
+
+ ,
+ )
+ expect(getByText("Continue")).toBeTruthy()
+ })
+
+ it("calls onCtaPress when button pressed", () => {
+ const { getByTestId } = render(
+
+
+ ,
+ )
+ fireEvent.press(getByTestId("cta-button"))
+ expect(mockOnCtaPress).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/__tests__/screens/account-migration/to-non-custodial/explainer-screen.spec.tsx b/__tests__/screens/account-migration/to-non-custodial/explainer-screen.spec.tsx
new file mode 100644
index 0000000000..291d51942a
--- /dev/null
+++ b/__tests__/screens/account-migration/to-non-custodial/explainer-screen.spec.tsx
@@ -0,0 +1,109 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react-native"
+import { loadLocale } from "@app/i18n/i18n-util.sync"
+
+import { SparkMigrationExplainerScreen } from "@app/screens/account-migration/to-non-custodial/explainer-screen"
+import { ContextForScreen } from "../../helper"
+
+loadLocale("en")
+
+const mockNavigate = jest.fn()
+
+jest.mock("@react-navigation/native", () => ({
+ ...jest.requireActual("@react-navigation/native"),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+}))
+
+jest.mock("@app/utils/external", () => ({
+ openExternalUrl: jest.fn(),
+}))
+
+jest.mock("@app/config/feature-flags-context", () => ({
+ useRemoteConfig: () => ({
+ sparkCompatibleWalletsUrl: "https://docs.spark.money/wallets/overview",
+ }),
+}))
+
+jest.mock("@app/components/icon-hero", () => ({
+ IconHero: ({ title }: { title: string }) => {
+ const { Text } = jest.requireActual("react-native")
+ return {title}
+ },
+}))
+
+describe("SparkMigrationExplainerScreen", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ loadLocale("en")
+ })
+
+ it("renders the explainer title", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("What does it mean to move to non-custodial?")).toBeTruthy()
+ })
+
+ it("renders all three steps", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("1.")).toBeTruthy()
+ expect(screen.getByText("2.")).toBeTruthy()
+ expect(screen.getByText("3.")).toBeTruthy()
+ })
+
+ it("renders learn more link", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("learn more here")).toBeTruthy()
+ })
+
+ it("opens external URL when learn more is pressed", () => {
+ const { openExternalUrl } = jest.requireMock("@app/utils/external")
+
+ render(
+
+
+ ,
+ )
+
+ fireEvent.press(screen.getByText("learn more here"))
+ expect(openExternalUrl).toHaveBeenCalledWith(
+ "https://docs.spark.money/wallets/overview",
+ )
+ })
+
+ it("renders Let's move button", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("Let's move")).toBeTruthy()
+ })
+
+ it("navigates to backup method on button press", () => {
+ render(
+
+
+ ,
+ )
+
+ fireEvent.press(screen.getByText("Let's move"))
+ expect(mockNavigate).toHaveBeenCalledWith("sparkBackupMethodScreen")
+ })
+})
diff --git a/__tests__/screens/account-migration/to-non-custodial/transferring-funds-screen.spec.tsx b/__tests__/screens/account-migration/to-non-custodial/transferring-funds-screen.spec.tsx
new file mode 100644
index 0000000000..ce6053b149
--- /dev/null
+++ b/__tests__/screens/account-migration/to-non-custodial/transferring-funds-screen.spec.tsx
@@ -0,0 +1,78 @@
+import React from "react"
+import { render, screen, act } from "@testing-library/react-native"
+import { loadLocale } from "@app/i18n/i18n-util.sync"
+
+import { TransferringFundsScreen } from "@app/screens/account-migration/to-non-custodial/transferring-funds-screen"
+import { ContextForScreen } from "../../helper"
+
+loadLocale("en")
+
+const mockReplace = jest.fn()
+
+jest.mock("@react-navigation/native", () => ({
+ ...jest.requireActual("@react-navigation/native"),
+ useNavigation: () => ({
+ replace: mockReplace,
+ }),
+}))
+
+jest.mock("@app/components/status-screen-layout", () => ({
+ StatusScreenLayout: ({ children }: { children: React.ReactNode }) => {
+ const { View } = jest.requireActual("react-native")
+ return {children}
+ },
+}))
+
+describe("TransferringFundsScreen", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ jest.useFakeTimers()
+ loadLocale("en")
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ it("renders transferring funds message", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(
+ screen.getByText("Transferring your funds. It should be done in a few seconds."),
+ ).toBeTruthy()
+ })
+
+ it("navigates to success screen after timeout", async () => {
+ render(
+
+
+ ,
+ )
+
+ expect(mockReplace).not.toHaveBeenCalled()
+
+ await act(() => {
+ jest.advanceTimersByTime(2000)
+ })
+
+ expect(mockReplace).toHaveBeenCalledWith("sparkBackupSuccessScreen")
+ })
+
+ it("does not navigate before timeout", async () => {
+ render(
+
+
+ ,
+ )
+
+ await act(() => {
+ jest.advanceTimersByTime(1000)
+ })
+
+ expect(mockReplace).not.toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/screens/account-migration/utils/migration-checkpoint-storage.spec.ts b/__tests__/screens/account-migration/utils/migration-checkpoint-storage.spec.ts
new file mode 100644
index 0000000000..ad3f987012
--- /dev/null
+++ b/__tests__/screens/account-migration/utils/migration-checkpoint-storage.spec.ts
@@ -0,0 +1,211 @@
+import { Platform } from "react-native"
+
+import {
+ MigrationCheckpoint,
+ clearCheckpointFromStorage,
+ getStorageKey,
+ isExpired,
+ loadCheckpoint,
+ resolveCheckpointRoute,
+ saveCheckpointToStorage,
+ validateStoredCheckpoint,
+} from "@app/screens/account-migration/utils/migration-checkpoint-storage"
+
+const mockLoadJson = jest.fn()
+const mockSaveJson = jest.fn()
+const mockRemove = jest.fn()
+
+jest.mock("@app/utils/storage", () => ({
+ loadJson: (...args: readonly unknown[]) => mockLoadJson(...args),
+ saveJson: (...args: readonly unknown[]) => mockSaveJson(...args),
+ remove: (...args: readonly unknown[]) => mockRemove(...args),
+}))
+
+describe("migration-checkpoint-storage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockRemove.mockResolvedValue(undefined)
+ })
+
+ describe("getStorageKey", () => {
+ it("namespaces by environment", () => {
+ expect(getStorageKey("Main")).toBe("migrationCheckpoint_main")
+ expect(getStorageKey("Staging")).toBe("migrationCheckpoint_staging")
+ })
+ })
+
+ describe("validateStoredCheckpoint", () => {
+ it("returns null for null input", () => {
+ expect(validateStoredCheckpoint(null)).toBeNull()
+ })
+
+ it("returns null for non-object input", () => {
+ expect(validateStoredCheckpoint("string")).toBeNull()
+ })
+
+ it("returns null for invalid step", () => {
+ expect(validateStoredCheckpoint({ step: "invalid", savedAt: 123 })).toBeNull()
+ })
+
+ it("returns null for missing savedAt", () => {
+ expect(validateStoredCheckpoint({ step: "backupMethod" })).toBeNull()
+ })
+
+ it("returns null for non-number savedAt", () => {
+ expect(
+ validateStoredCheckpoint({ step: "backupMethod", savedAt: "not-a-number" }),
+ ).toBeNull()
+ })
+
+ it("returns valid checkpoint", () => {
+ const result = validateStoredCheckpoint({ step: "backupMethod", savedAt: 1000 })
+ expect(result).toEqual({ step: "backupMethod", savedAt: 1000 })
+ })
+ })
+
+ describe("isExpired (48h uniform)", () => {
+ const now = 1000000000
+ const h = 60 * 60 * 1000
+
+ it("not expired at 24h", () => {
+ const cp = { step: MigrationCheckpoint.BackupMethod, savedAt: now - 24 * h }
+ expect(isExpired(cp, now)).toBe(false)
+ })
+
+ it("not expired at 47h", () => {
+ const cp = { step: MigrationCheckpoint.CloudBackup, savedAt: now - 47 * h }
+ expect(isExpired(cp, now)).toBe(false)
+ })
+
+ it("not expired at 1h", () => {
+ const cp = { step: MigrationCheckpoint.BackupAlerts, savedAt: now - Number(h) }
+ expect(isExpired(cp, now)).toBe(false)
+ })
+
+ it("expired at 49h for BackupMethod", () => {
+ const cp = { step: MigrationCheckpoint.BackupMethod, savedAt: now - 49 * h }
+ expect(isExpired(cp, now)).toBe(true)
+ })
+
+ it("expired at 49h for CloudBackup", () => {
+ const cp = { step: MigrationCheckpoint.CloudBackup, savedAt: now - 49 * h }
+ expect(isExpired(cp, now)).toBe(true)
+ })
+
+ it("expired at 49h for BackupAlerts", () => {
+ const cp = { step: MigrationCheckpoint.BackupAlerts, savedAt: now - 49 * h }
+ expect(isExpired(cp, now)).toBe(true)
+ })
+ })
+
+ describe("resolveCheckpointRoute", () => {
+ it("returns default for null checkpoint", () => {
+ expect(resolveCheckpointRoute(null)).toBe("sparkMigrationExplainer")
+ })
+
+ it("returns correct route for BackupMethod", () => {
+ expect(resolveCheckpointRoute(MigrationCheckpoint.BackupMethod)).toBe(
+ "sparkBackupMethodScreen",
+ )
+ })
+
+ it("returns correct route for BackupAlerts", () => {
+ expect(resolveCheckpointRoute(MigrationCheckpoint.BackupAlerts)).toBe(
+ "sparkBackupAlertsScreen",
+ )
+ })
+
+ it("returns correct route for CloudBackup on Android", () => {
+ const original = Platform.OS
+ Object.defineProperty(Platform, "OS", { value: "android" })
+
+ expect(resolveCheckpointRoute(MigrationCheckpoint.CloudBackup)).toBe(
+ "sparkCloudBackupScreen",
+ )
+
+ Object.defineProperty(Platform, "OS", { value: original })
+ })
+
+ it("returns default route for CloudBackup on iOS", () => {
+ const original = Platform.OS
+ Object.defineProperty(Platform, "OS", { value: "ios" })
+
+ expect(resolveCheckpointRoute(MigrationCheckpoint.CloudBackup)).toBe(
+ "sparkMigrationExplainer",
+ )
+
+ Object.defineProperty(Platform, "OS", { value: original })
+ })
+ })
+
+ describe("loadCheckpoint", () => {
+ it("returns valid non-expired checkpoint", async () => {
+ mockLoadJson.mockResolvedValue({
+ step: "backupAlerts",
+ savedAt: Date.now() - 1000,
+ })
+
+ const result = await loadCheckpoint("test-key")
+ expect(result).toEqual({
+ step: "backupAlerts",
+ savedAt: expect.any(Number),
+ })
+ })
+
+ it("returns null and removes expired checkpoint", async () => {
+ mockLoadJson.mockResolvedValue({
+ step: "backupMethod",
+ savedAt: Date.now() - 49 * 60 * 60 * 1000,
+ })
+
+ const result = await loadCheckpoint("test-key")
+ expect(result).toBeNull()
+ expect(mockRemove).toHaveBeenCalledWith("test-key")
+ })
+
+ it("returns null for invalid data", async () => {
+ mockLoadJson.mockResolvedValue({ step: "invalid" })
+
+ const result = await loadCheckpoint("test-key")
+ expect(result).toBeNull()
+ })
+
+ it("returns null and clears on storage error", async () => {
+ mockLoadJson.mockRejectedValue(new Error("corrupt"))
+
+ const result = await loadCheckpoint("test-key")
+ expect(result).toBeNull()
+ expect(mockRemove).toHaveBeenCalledWith("test-key")
+ })
+
+ it("returns null for null storage", async () => {
+ mockLoadJson.mockResolvedValue(null)
+
+ const result = await loadCheckpoint("test-key")
+ expect(result).toBeNull()
+ })
+ })
+
+ describe("saveCheckpointToStorage", () => {
+ it("persists step and timestamp", async () => {
+ const before = Date.now()
+ await saveCheckpointToStorage("test-key", MigrationCheckpoint.BackupAlerts)
+
+ expect(mockSaveJson).toHaveBeenCalledWith("test-key", {
+ step: MigrationCheckpoint.BackupAlerts,
+ savedAt: expect.any(Number),
+ })
+
+ const savedAt = mockSaveJson.mock.calls[0][1].savedAt
+ expect(savedAt).toBeGreaterThanOrEqual(before)
+ expect(savedAt).toBeLessThanOrEqual(Date.now())
+ })
+ })
+
+ describe("clearCheckpointFromStorage", () => {
+ it("removes key from storage", async () => {
+ await clearCheckpointFromStorage("test-key")
+ expect(mockRemove).toHaveBeenCalledWith("test-key")
+ })
+ })
+})
diff --git a/__tests__/screens/settings-screen/settings-screen.spec.tsx b/__tests__/screens/settings-screen/settings-screen.spec.tsx
index 710130be39..f4e022446f 100644
--- a/__tests__/screens/settings-screen/settings-screen.spec.tsx
+++ b/__tests__/screens/settings-screen/settings-screen.spec.tsx
@@ -511,4 +511,21 @@ describe("Settings Screen", () => {
expect(subtitleNode.props.numberOfLines).toBe(1)
expect(subtitleNode.props.ellipsizeMode).toBe("tail")
})
+
+ it("renders Move to non-custodial option in Account section", async () => {
+ render(
+
+
+ ,
+ )
+
+ await act(
+ () =>
+ new Promise((resolve) => {
+ setTimeout(resolve, 10)
+ }),
+ )
+
+ expect(screen.getByText("Move to non-custodial")).toBeTruthy()
+ })
})
diff --git a/__tests__/screens/spark-onboarding/backup-alerts-screen.spec.tsx b/__tests__/screens/spark-onboarding/backup-alerts-screen.spec.tsx
index 8dca0f757b..509662d8ac 100644
--- a/__tests__/screens/spark-onboarding/backup-alerts-screen.spec.tsx
+++ b/__tests__/screens/spark-onboarding/backup-alerts-screen.spec.tsx
@@ -12,6 +12,15 @@ jest.mock("@react-navigation/native", () => ({
useNavigation: () => ({ navigate: mockNavigate }),
}))
+const mockSaveCheckpoint = jest.fn()
+
+jest.mock("@app/screens/account-migration/hooks", () => ({
+ useMigrationCheckpoint: () => ({
+ saveCheckpoint: mockSaveCheckpoint,
+ }),
+ MigrationCheckpoint: { BackupAlerts: "backupAlerts" },
+}))
+
jest.mock("@app/components/icon-hero", () => {
const { Text } = jest.requireActual("react-native")
return {
@@ -105,4 +114,14 @@ describe("SparkBackupAlertsScreen", () => {
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check2())).toBeTruthy()
expect(getByText(LL.SparkOnboarding.ManualBackup.Alerts.check3())).toBeTruthy()
})
+
+ it("saves BackupAlerts checkpoint on mount", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(mockSaveCheckpoint).toHaveBeenCalledWith("backupAlerts")
+ })
})
diff --git a/__tests__/screens/spark-onboarding/backup-confirm-screen.spec.tsx b/__tests__/screens/spark-onboarding/backup-confirm-screen.spec.tsx
index 1adf20dde8..0df5edbab3 100644
--- a/__tests__/screens/spark-onboarding/backup-confirm-screen.spec.tsx
+++ b/__tests__/screens/spark-onboarding/backup-confirm-screen.spec.tsx
@@ -11,6 +11,28 @@ jest.mock("react-native-inappbrowser-reborn", () => ({
default: { open: jest.fn(() => Promise.resolve()) },
}))
+jest.mock("@app/screens/account-migration/hooks", () => ({
+ useMigrationCheckpoint: () => ({ saveCheckpoint: jest.fn() }),
+ MigrationCheckpoint: {
+ BackupMethod: "backupMethod",
+ CloudBackup: "cloudBackup",
+ BackupAlerts: "backupAlerts",
+ },
+}))
+
+jest.mock("@app/graphql/generated", () => ({
+ ...jest.requireActual("@app/graphql/generated"),
+ useHomeAuthedQuery: () => ({
+ data: {
+ me: {
+ defaultAccount: {
+ wallets: [{ balance: 1000, walletCurrency: "BTC" }],
+ },
+ },
+ },
+ }),
+}))
+
const mockNavigate = jest.fn()
jest.mock("@react-navigation/native", () => ({
...jest.requireActual("@react-navigation/native"),
diff --git a/__tests__/screens/spark-onboarding/backup-method-screen.spec.tsx b/__tests__/screens/spark-onboarding/backup-method-screen.spec.tsx
index 309e1355b6..390cd84db7 100644
--- a/__tests__/screens/spark-onboarding/backup-method-screen.spec.tsx
+++ b/__tests__/screens/spark-onboarding/backup-method-screen.spec.tsx
@@ -23,6 +23,15 @@ jest.mock("@app/screens/spark-onboarding/hooks", () => ({
}),
}))
+const mockSaveCheckpoint = jest.fn()
+
+jest.mock("@app/screens/account-migration/hooks", () => ({
+ useMigrationCheckpoint: () => ({
+ saveCheckpoint: mockSaveCheckpoint,
+ }),
+ MigrationCheckpoint: { BackupMethod: "backupMethod" },
+}))
+
jest.mock("@app/components/atomic/galoy-primary-button", () => ({
GaloyPrimaryButton: ({
title,
@@ -152,4 +161,14 @@ describe("SparkBackupMethodScreen", () => {
).toEqual({ disabled: true })
expect(getByText(LL.SparkOnboarding.BackupMethod.iOSComingSoon())).toBeTruthy()
})
+
+ it("saves BackupMethod checkpoint on mount", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(mockSaveCheckpoint).toHaveBeenCalledWith("backupMethod")
+ })
})
diff --git a/__tests__/screens/spark-onboarding/backup-success-screen.spec.tsx b/__tests__/screens/spark-onboarding/backup-success-screen.spec.tsx
index 54299c564c..47eb5fab99 100644
--- a/__tests__/screens/spark-onboarding/backup-success-screen.spec.tsx
+++ b/__tests__/screens/spark-onboarding/backup-success-screen.spec.tsx
@@ -31,6 +31,13 @@ jest.mock("@app/components/success-animation/success-text-animation", () => {
}
})
+const mockClearCheckpoint = jest.fn()
+jest.mock("@app/screens/account-migration/hooks", () => ({
+ useMigrationCheckpoint: () => ({
+ clearCheckpoint: mockClearCheckpoint,
+ }),
+}))
+
const mockDispatch = jest.fn()
jest.mock("@react-navigation/native", () => ({
...jest.requireActual("@react-navigation/native"),
@@ -89,4 +96,18 @@ describe("SparkBackupSuccessScreen", () => {
}),
)
})
+
+ it("clears migration checkpoint on navigation", async () => {
+ render(
+
+
+ ,
+ )
+
+ await new Promise((resolve) => {
+ setTimeout(resolve, 50)
+ })
+
+ expect(mockClearCheckpoint).toHaveBeenCalled()
+ })
})
diff --git a/__tests__/utils/external.spec.ts b/__tests__/utils/external.spec.ts
new file mode 100644
index 0000000000..5a391c58ba
--- /dev/null
+++ b/__tests__/utils/external.spec.ts
@@ -0,0 +1,48 @@
+import { Linking } from "react-native"
+
+import { openExternalUrl, openWhatsApp } from "@app/utils/external"
+
+const mockOpen = jest.fn()
+
+jest.mock("react-native-inappbrowser-reborn", () => ({
+ default: { open: (...args: readonly unknown[]) => mockOpen(...args) },
+ __esModule: true,
+}))
+
+jest.spyOn(Linking, "openURL").mockResolvedValue(undefined)
+
+describe("openExternalUrl", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("opens URL in InAppBrowser", async () => {
+ mockOpen.mockResolvedValue(undefined)
+ await openExternalUrl("https://example.com")
+
+ expect(mockOpen).toHaveBeenCalledWith("https://example.com")
+ expect(Linking.openURL).not.toHaveBeenCalled()
+ })
+
+ it("falls back to Linking when InAppBrowser fails", async () => {
+ mockOpen.mockRejectedValue(new Error("unavailable"))
+ await openExternalUrl("https://example.com")
+
+ expect(mockOpen).toHaveBeenCalledWith("https://example.com")
+ expect(Linking.openURL).toHaveBeenCalledWith("https://example.com")
+ })
+})
+
+describe("openWhatsApp", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("opens whatsapp URL with encoded phone and message", async () => {
+ await openWhatsApp("+1234567890", "Hello World")
+
+ expect(Linking.openURL).toHaveBeenCalledWith(
+ "whatsapp://send?phone=%2B1234567890&text=Hello%20World",
+ )
+ })
+})
diff --git a/app/components/atomic/galoy-icon/galoy-icon.tsx b/app/components/atomic/galoy-icon/galoy-icon.tsx
index ee7383ccda..5413e9398c 100644
--- a/app/components/atomic/galoy-icon/galoy-icon.tsx
+++ b/app/components/atomic/galoy-icon/galoy-icon.tsx
@@ -15,6 +15,7 @@ import {
BookOpenIcon,
CalculatorIcon,
CalendarIcon,
+ ClockIcon,
CaretDownIcon,
CaretLeftIcon,
CaretRightIcon,
@@ -132,6 +133,7 @@ const phosphorIconMap = {
"bank": BankIcon,
"bell": BellIcon,
"book": BookIcon,
+ "clock": ClockIcon,
"brush": PaintBrushIcon,
"book-open": BookOpenIcon,
"calculator": CalculatorIcon,
diff --git a/app/components/icon-hero/icon-hero.tsx b/app/components/icon-hero/icon-hero.tsx
index 6ad9bdd7be..8f8a1b7503 100644
--- a/app/components/icon-hero/icon-hero.tsx
+++ b/app/components/icon-hero/icon-hero.tsx
@@ -50,7 +50,6 @@ const useStyles = makeStyles(({ colors }) => ({
textContainer: {
alignItems: "center",
gap: 8,
- width: 264,
},
title: {
fontSize: 20,
diff --git a/app/components/numbered-steps-list/index.ts b/app/components/numbered-steps-list/index.ts
new file mode 100644
index 0000000000..250c92e6d2
--- /dev/null
+++ b/app/components/numbered-steps-list/index.ts
@@ -0,0 +1 @@
+export { NumberedStepsList } from "./numbered-steps-list"
diff --git a/app/components/numbered-steps-list/numbered-steps-list.tsx b/app/components/numbered-steps-list/numbered-steps-list.tsx
new file mode 100644
index 0000000000..7e9c696d7d
--- /dev/null
+++ b/app/components/numbered-steps-list/numbered-steps-list.tsx
@@ -0,0 +1,45 @@
+import React from "react"
+import { Text, View } from "react-native"
+
+import { makeStyles } from "@rn-vui/themed"
+
+type NumberedStepsListProps = {
+ steps: ReadonlyArray
+}
+
+export const NumberedStepsList: React.FC = ({ steps }) => {
+ const styles = useStyles()
+
+ return (
+
+ {steps.map((content, index) => (
+
+ {`${index + 1}.`}
+ {content}
+
+ ))}
+
+ )
+}
+
+const useStyles = makeStyles(({ colors }) => ({
+ container: {
+ gap: 4,
+ paddingTop: 14,
+ },
+ row: {
+ flexDirection: "row",
+ },
+ base: {
+ fontSize: 16,
+ fontWeight: "400",
+ lineHeight: 22,
+ color: colors.black,
+ },
+ number: {
+ width: 20,
+ },
+ text: {
+ flex: 1,
+ },
+}))
diff --git a/app/components/rich-text/rich-text.tsx b/app/components/rich-text/rich-text.tsx
index 968a9851a8..b5f909353e 100644
--- a/app/components/rich-text/rich-text.tsx
+++ b/app/components/rich-text/rich-text.tsx
@@ -1,28 +1,45 @@
import React from "react"
+import { StyleProp, TextStyle } from "react-native"
import { makeStyles, Text } from "@rn-vui/themed"
+type TagHandler = {
+ style?: StyleProp
+ onPress?: () => void
+}
+
type RichTextProps = {
text: string
- bold: string
+ tags?: Record
+ style?: StyleProp
}
-export const RichText: React.FC = ({ text, bold }) => {
+const SPLIT_PATTERN = /(<\w+>.*?<\/\w+>)/g
+const TAG_PATTERN = /^<(\w+)>(.*)<\/\1>$/
+
+export const RichText: React.FC = ({ text, tags, style }) => {
const styles = useStyles()
- if (!text.includes(bold)) {
- return {text}
+ const allTags: Record = {
+ bold: { style: styles.bold },
+ link: { style: styles.link },
+ ...tags,
}
- const [before, after] = text.split(bold)
+ const parts = text.split(SPLIT_PATTERN).map((part, i) => {
+ const match = part.match(TAG_PATTERN)
+ if (!match) return part
- return (
-
- {before}
- {bold}
- {after}
-
- )
+ const [, tag, inner] = match
+ const handler = allTags[tag]
+ return (
+
+ {inner}
+
+ )
+ })
+
+ return {parts}
}
const useStyles = makeStyles(({ colors }) => ({
@@ -35,4 +52,8 @@ const useStyles = makeStyles(({ colors }) => ({
fontWeight: "700",
color: colors.black,
},
+ link: {
+ textDecorationLine: "underline",
+ color: colors.black,
+ },
}))
diff --git a/app/components/status-screen-layout/index.ts b/app/components/status-screen-layout/index.ts
new file mode 100644
index 0000000000..5378a0ed43
--- /dev/null
+++ b/app/components/status-screen-layout/index.ts
@@ -0,0 +1 @@
+export { StatusScreenLayout } from "./status-screen-layout"
diff --git a/app/components/status-screen-layout/status-screen-layout.tsx b/app/components/status-screen-layout/status-screen-layout.tsx
new file mode 100644
index 0000000000..0d28f26650
--- /dev/null
+++ b/app/components/status-screen-layout/status-screen-layout.tsx
@@ -0,0 +1,64 @@
+import React from "react"
+import { View } from "react-native"
+
+import { makeStyles } from "@rn-vui/themed"
+
+import { GaloyIcon, IconNamesType } from "@app/components/atomic/galoy-icon"
+
+type StatusScreenLayoutProps = {
+ icon: IconNamesType
+ iconSize?: number
+ iconBackgroundColor?: string
+ children: React.ReactNode
+ footer?: React.ReactNode
+}
+
+export const StatusScreenLayout: React.FC = ({
+ icon,
+ iconSize = 72,
+ iconBackgroundColor = "transparent",
+ children,
+ footer,
+}) => {
+ const styles = useStyles({ iconBackgroundColor })
+
+ return (
+
+
+
+
+
+ {children}
+
+ {footer ? {footer} : null}
+
+ )
+}
+
+type StyleProps = {
+ iconBackgroundColor: string
+}
+
+const useStyles = makeStyles((_theme, { iconBackgroundColor }: StyleProps) => ({
+ container: {
+ flex: 1,
+ justifyContent: "space-between",
+ },
+ content: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ paddingHorizontal: 40,
+ gap: 26,
+ },
+ iconCircle: {
+ padding: 10,
+ borderRadius: 59,
+ backgroundColor: iconBackgroundColor,
+ },
+ footer: {
+ paddingHorizontal: 20,
+ paddingBottom: 20,
+ paddingTop: 10,
+ },
+}))
diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts
index 571abf743d..8be0decbe2 100644
--- a/app/i18n/en/index.ts
+++ b/app/i18n/en/index.ts
@@ -3632,6 +3632,18 @@ const en: BaseTranslation = {
},
},
},
+ AccountMigration: {
+ moveToNonCustodial: "Move to non-custodial",
+ explainerTitle: "What does it mean to move to non-custodial?",
+ explainerStep1:
+ "You will create a non-custodial account on the Spark protocol, learn more here",
+ explainerStep2:
+ "We transfer your funds into your new non-custodial account, and your current account will be deleted",
+ explainerStep3: "Continue using Blink as usual",
+ letsMove: "Let's move",
+ transferringFunds:
+ "Transferring your funds. It should be done in a few seconds.",
+ },
}
export default en
diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts
index a58c43fa0a..ccce87ccb3 100644
--- a/app/i18n/i18n-types.ts
+++ b/app/i18n/i18n-types.ts
@@ -11498,6 +11498,36 @@ type RootTranslation = {
}
}
}
+ AccountMigration: {
+ /**
+ * Move to non-custodial
+ */
+ moveToNonCustodial: string
+ /**
+ * What does it mean to move to non-custodial?
+ */
+ explainerTitle: string
+ /**
+ * You will create a non-custodial account on the Spark protocol, <link>learn more here</link>
+ */
+ explainerStep1: string
+ /**
+ * We transfer your funds into your new non-custodial account, and your current account will be deleted
+ */
+ explainerStep2: string
+ /**
+ * Continue using Blink as usual
+ */
+ explainerStep3: string
+ /**
+ * Let's move
+ */
+ letsMove: string
+ /**
+ * Transferring your funds. It should be done in a few seconds.
+ */
+ transferringFunds: string
+ }
}
export type TranslationFunctions = {
@@ -22846,6 +22876,36 @@ export type TranslationFunctions = {
}
}
}
+ AccountMigration: {
+ /**
+ * Move to non-custodial
+ */
+ moveToNonCustodial: () => LocalizedString
+ /**
+ * What does it mean to move to non-custodial?
+ */
+ explainerTitle: () => LocalizedString
+ /**
+ * You will create a non-custodial account on the Spark protocol, learn more here
+ */
+ explainerStep1: () => LocalizedString
+ /**
+ * We transfer your funds into your new non-custodial account, and your current account will be deleted
+ */
+ explainerStep2: () => LocalizedString
+ /**
+ * Continue using Blink as usual
+ */
+ explainerStep3: () => LocalizedString
+ /**
+ * Let's move
+ */
+ letsMove: () => LocalizedString
+ /**
+ * Transferring your funds. It should be done in a few seconds.
+ */
+ transferringFunds: () => LocalizedString
+ }
}
export type Formatters = {
diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json
index 39c66967be..4e4b7a4202 100644
--- a/app/i18n/raw-i18n/source/en.json
+++ b/app/i18n/raw-i18n/source/en.json
@@ -3483,5 +3483,14 @@
"title": "Welcome to non-custodial Blink"
}
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Move to non-custodial",
+ "explainerTitle": "What does it mean to move to non-custodial?",
+ "explainerStep1": "You will create a non-custodial account on the Spark protocol, learn more here",
+ "explainerStep2": "We transfer your funds into your new non-custodial account, and your current account will be deleted",
+ "explainerStep3": "Continue using Blink as usual",
+ "letsMove": "Let's move",
+ "transferringFunds": "Transferring your funds. It should be done in a few seconds."
}
}
diff --git a/app/i18n/raw-i18n/translations/af.json b/app/i18n/raw-i18n/translations/af.json
index 2236ae2806..02f8dc0de4 100644
--- a/app/i18n/raw-i18n/translations/af.json
+++ b/app/i18n/raw-i18n/translations/af.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "'n Rugsteun bestaan reeds in jou {provider}. Wil jy dit oorskryf?",
"overwrite": "Oorskryf"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Skuif na nie-bewaarde",
+ "explainerTitle": "Wat beteken dit om na nie-bewaarde te skuif?",
+ "explainerStep1": "Jy sal 'n nie-bewaarde rekening op die Spark-protokol skep, leer meer hier",
+ "explainerStep2": "Ons dra jou fondse oor na jou nuwe nie-bewaarde rekening en jou huidige rekening sal verwyder word",
+ "explainerStep3": "Gebruik Blink voort soos gewoonlik",
+ "letsMove": "Kom ons gaan",
+ "transferringFunds": "Jou fondse word oorgedra. Dit behoort binne 'n paar sekondes klaar te wees."
}
}
diff --git a/app/i18n/raw-i18n/translations/ar.json b/app/i18n/raw-i18n/translations/ar.json
index ff60aaee85..8eb2881c69 100644
--- a/app/i18n/raw-i18n/translations/ar.json
+++ b/app/i18n/raw-i18n/translations/ar.json
@@ -3531,5 +3531,14 @@
"existingBackupMessage": "توجد نسخة احتياطية بالفعل في {provider} الخاص بك. هل تريد استبدالها؟",
"overwrite": "استبدال"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "الانتقال إلى غير حفظي",
+ "explainerTitle": "ماذا يعني الانتقال إلى غير حفظي؟",
+ "explainerStep1": "ستقوم بإنشاء حساب غير حفظي على بروتوكول Spark، اعرف المزيد هنا",
+ "explainerStep2": "سننقل أموالك إلى حسابك الجديد غير الحفظي وسيتم حذف حسابك الحالي",
+ "explainerStep3": "استمر في استخدام Blink كالمعتاد",
+ "letsMove": "هيا بنا",
+ "transferringFunds": "جارٍ تحويل أموالك. سيتم الانتهاء في غضون ثوانٍ قليلة."
}
}
diff --git a/app/i18n/raw-i18n/translations/ca.json b/app/i18n/raw-i18n/translations/ca.json
index e1342f9a93..e82abae987 100644
--- a/app/i18n/raw-i18n/translations/ca.json
+++ b/app/i18n/raw-i18n/translations/ca.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Ja existeix una còpia de seguretat al teu {provider}. Vols sobreescriure-la?",
"overwrite": "Sobreescriure"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Passa a no custodial",
+ "explainerTitle": "Què significa passar a no custodial?",
+ "explainerStep1": "Crearàs un compte no custodial al protocol Spark, més informació aquí",
+ "explainerStep2": "Transferirem els teus fons al teu nou compte no custodial i el teu compte actual serà eliminat",
+ "explainerStep3": "Continua fent servir Blink com sempre",
+ "letsMove": "Anem-hi",
+ "transferringFunds": "Transferint els teus fons. Hauria d'estar llest en uns segons."
}
}
diff --git a/app/i18n/raw-i18n/translations/cs.json b/app/i18n/raw-i18n/translations/cs.json
index c154ea3fbb..1aa0a7185d 100644
--- a/app/i18n/raw-i18n/translations/cs.json
+++ b/app/i18n/raw-i18n/translations/cs.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "Ve vašem {provider} již existuje záloha. Chcete ji přepsat?",
"overwrite": "Přepsat"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Přejít na non-custodial",
+ "explainerTitle": "Co znamená přejít na non-custodial?",
+ "explainerStep1": "Vytvoříte non-custodial účet na protokolu Spark, zjistit více zde",
+ "explainerStep2": "Převedeme vaše prostředky na váš nový non-custodial účet a váš stávající účet bude smazán",
+ "explainerStep3": "Pokračujte v používání Blink jako obvykle",
+ "letsMove": "Pojďme na to",
+ "transferringFunds": "Převádíme vaše prostředky. Mělo by to být hotové za několik sekund."
}
}
diff --git a/app/i18n/raw-i18n/translations/da.json b/app/i18n/raw-i18n/translations/da.json
index d47b11ab46..b742429a76 100644
--- a/app/i18n/raw-i18n/translations/da.json
+++ b/app/i18n/raw-i18n/translations/da.json
@@ -3511,5 +3511,14 @@
"existingBackupMessage": "Der findes allerede en sikkerhedskopi i din {provider}. Vil du overskrive den?",
"overwrite": "Overskriv"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Flyt til non-custodial",
+ "explainerTitle": "Hvad betyder det at flytte til non-custodial?",
+ "explainerStep1": "Du opretter en non-custodial konto på Spark-protokollen, lær mere her",
+ "explainerStep2": "Vi overfører dine midler til din nye non-custodial konto, og din nuværende konto vil blive slettet",
+ "explainerStep3": "Fortsæt med at bruge Blink som normalt",
+ "letsMove": "Lad os gå",
+ "transferringFunds": "Overfører dine midler. Det skulle være færdigt om få sekunder."
}
}
diff --git a/app/i18n/raw-i18n/translations/de.json b/app/i18n/raw-i18n/translations/de.json
index 87a013e7b8..cd37a08cb4 100644
--- a/app/i18n/raw-i18n/translations/de.json
+++ b/app/i18n/raw-i18n/translations/de.json
@@ -3481,5 +3481,14 @@
"existingBackupMessage": "Es existiert bereits eine Sicherung in Ihrem {provider}. Möchten Sie sie überschreiben?",
"overwrite": "Überschreiben"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Zu Non-Custodial wechseln",
+ "explainerTitle": "Was bedeutet es, zu Non-Custodial zu wechseln?",
+ "explainerStep1": "Du erstellst ein Non-Custodial-Konto auf dem Spark-Protokoll, hier mehr erfahren",
+ "explainerStep2": "Wir übertragen deine Guthaben auf dein neues Non-Custodial-Konto und dein aktuelles Konto wird gelöscht",
+ "explainerStep3": "Nutze Blink weiterhin wie gewohnt",
+ "letsMove": "Los geht's",
+ "transferringFunds": "Deine Guthaben werden übertragen. Es sollte in wenigen Sekunden abgeschlossen sein."
}
}
diff --git a/app/i18n/raw-i18n/translations/el.json b/app/i18n/raw-i18n/translations/el.json
index 0d86175cc5..e3782514c5 100644
--- a/app/i18n/raw-i18n/translations/el.json
+++ b/app/i18n/raw-i18n/translations/el.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Υπάρχει ήδη αντίγραφο ασφαλείας στο {provider} σας. Θέλετε να το αντικαταστήσετε;",
"overwrite": "Αντικατάσταση"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Μετάβαση σε non-custodial",
+ "explainerTitle": "Τι σημαίνει η μετάβαση σε non-custodial;",
+ "explainerStep1": "Θα δημιουργήσετε έναν non-custodial λογαριασμό στο πρωτόκολλο Spark, μάθετε περισσότερα εδώ",
+ "explainerStep2": "Θα μεταφέρουμε τα κεφάλαιά σας στον νέο σας non-custodial λογαριασμό και ο τρέχων λογαριασμός σας θα διαγραφεί",
+ "explainerStep3": "Συνεχίστε να χρησιμοποιείτε το Blink κανονικά",
+ "letsMove": "Πάμε",
+ "transferringFunds": "Μεταφορά των κεφαλαίων σας. Θα ολοκληρωθεί σε λίγα δευτερόλεπτα."
}
}
diff --git a/app/i18n/raw-i18n/translations/es.json b/app/i18n/raw-i18n/translations/es.json
index de4d87a9f3..45331bcd4c 100644
--- a/app/i18n/raw-i18n/translations/es.json
+++ b/app/i18n/raw-i18n/translations/es.json
@@ -3481,5 +3481,14 @@
"existingBackupMessage": "Ya existe un respaldo en tu {provider}. ¿Deseas sobrescribirlo?",
"overwrite": "Sobrescribir"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Migrar a no custodia",
+ "explainerTitle": "¿Qué significa migrar a no custodia?",
+ "explainerStep1": "Crearás una cuenta no custodial en el protocolo Spark, aprende más aquí",
+ "explainerStep2": "Transferiremos tus fondos a tu nueva cuenta no custodial y tu cuenta actual será eliminada",
+ "explainerStep3": "Continúa usando Blink como siempre",
+ "letsMove": "¡Vamos!",
+ "transferringFunds": "Transfiriendo tus fondos. Debería completarse en unos segundos."
}
}
diff --git a/app/i18n/raw-i18n/translations/fr.json b/app/i18n/raw-i18n/translations/fr.json
index efc7c1b1fb..aaf74b44d5 100644
--- a/app/i18n/raw-i18n/translations/fr.json
+++ b/app/i18n/raw-i18n/translations/fr.json
@@ -3522,5 +3522,14 @@
"existingBackupMessage": "Une sauvegarde existe déjà dans votre {provider}. Voulez-vous la remplacer ?",
"overwrite": "Remplacer"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Passer au non-custodial",
+ "explainerTitle": "Que signifie passer au non-custodial ?",
+ "explainerStep1": "Vous allez créer un compte non-custodial sur le protocole Spark, en savoir plus ici",
+ "explainerStep2": "Nous transférons vos fonds vers votre nouveau compte non-custodial et votre compte actuel sera supprimé",
+ "explainerStep3": "Continuez à utiliser Blink comme d'habitude",
+ "letsMove": "Allons-y",
+ "transferringFunds": "Transfert de vos fonds en cours. Cela devrait être terminé en quelques secondes."
}
}
diff --git a/app/i18n/raw-i18n/translations/hr.json b/app/i18n/raw-i18n/translations/hr.json
index bc7d7a4202..a1a4cbb3b4 100644
--- a/app/i18n/raw-i18n/translations/hr.json
+++ b/app/i18n/raw-i18n/translations/hr.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "Sigurnosna kopija već postoji u vašem {provider}. Želite li je prebrisati?",
"overwrite": "Prebriši"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Prebaci na ne-skrbnički",
+ "explainerTitle": "Što znači prebaciti na ne-skrbnički?",
+ "explainerStep1": "Stvorit ćete ne-skrbnički račun na Spark protokolu, saznajte više ovdje",
+ "explainerStep2": "Prebacujemo vaša sredstva na vaš novi ne-skrbnički račun, a vaš trenutni račun bit će izbrisan",
+ "explainerStep3": "Nastavite koristiti Blink kao i obično",
+ "letsMove": "Idemo",
+ "transferringFunds": "Prebacujemo vaša sredstva. Trebalo bi biti gotovo za nekoliko sekundi."
}
}
diff --git a/app/i18n/raw-i18n/translations/hu.json b/app/i18n/raw-i18n/translations/hu.json
index cc00467ffa..0fcb73036e 100644
--- a/app/i18n/raw-i18n/translations/hu.json
+++ b/app/i18n/raw-i18n/translations/hu.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Már létezik biztonsági mentés a {provider} tárhelyeden. Felül szeretnéd írni?",
"overwrite": "Felülírás"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Áttérés non-custodial-ra",
+ "explainerTitle": "Mit jelent a non-custodial-ra való áttérés?",
+ "explainerStep1": "Létrehozol egy non-custodial fiókot a Spark protokollon, tudj meg többet itt",
+ "explainerStep2": "Átutaljuk az egyenlegedet az új non-custodial fiókodba, és a jelenlegi fiókod törlésre kerül",
+ "explainerStep3": "Használd tovább a Blinket a megszokott módon",
+ "letsMove": "Rajta",
+ "transferringFunds": "Átutalás folyamatban. Néhány másodperc alatt befejeződik."
}
}
diff --git a/app/i18n/raw-i18n/translations/hy.json b/app/i18n/raw-i18n/translations/hy.json
index 02c3f83d64..ceb4185969 100644
--- a/app/i18n/raw-i18n/translations/hy.json
+++ b/app/i18n/raw-i18n/translations/hy.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "Ձեր {provider}-ում արդեն պահուստային պատճեն կա: Ուզում եք վերագրե՞լ:",
"overwrite": "Վերագրել"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Տեղափոխվել ոչ պահառուի",
+ "explainerTitle": "Ի՞նչ է նշանակում տեղափոխվել ոչ պահառուի:",
+ "explainerStep1": "Դուք կստեղծեք ոչ պահառու հաշիվ Spark արձանագրության վրա, իմացեք ավելին այստեղ",
+ "explainerStep2": "Մենք կփոխանցենք ձեր միջոցները ձեր նոր ոչ պահառու հաշվին, իսկ ձեր ընթացիկ հաշիվը կհեռացվի",
+ "explainerStep3": "Շարունակեք օգտագործել Blink-ը ինչպես միշտ",
+ "letsMove": "Եկեք տեղափոխվենք",
+ "transferringFunds": "Ձեր միջոցները փոխանցվում են։ Դա պետք է ավարտվի մի քանի վայրկյանի ընթացքում։"
}
}
diff --git a/app/i18n/raw-i18n/translations/id.json b/app/i18n/raw-i18n/translations/id.json
index 7e2677133a..c2a89505a2 100644
--- a/app/i18n/raw-i18n/translations/id.json
+++ b/app/i18n/raw-i18n/translations/id.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Cadangan sudah ada di {provider} Anda. Apakah Anda ingin menimpanya?",
"overwrite": "Timpa"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Pindah ke non-custodial",
+ "explainerTitle": "Apa artinya pindah ke non-custodial?",
+ "explainerStep1": "Anda akan membuat akun non-custodial di protokol Spark, pelajari lebih lanjut di sini",
+ "explainerStep2": "Kami mentransfer dana Anda ke akun non-custodial baru dan akun Anda saat ini akan dihapus",
+ "explainerStep3": "Terus gunakan Blink seperti biasa",
+ "letsMove": "Ayo mulai",
+ "transferringFunds": "Mentransfer dana Anda. Seharusnya selesai dalam beberapa detik."
}
}
diff --git a/app/i18n/raw-i18n/translations/it.json b/app/i18n/raw-i18n/translations/it.json
index 328609d084..17656648ec 100644
--- a/app/i18n/raw-i18n/translations/it.json
+++ b/app/i18n/raw-i18n/translations/it.json
@@ -3481,5 +3481,14 @@
"existingBackupMessage": "Esiste già un backup nel tuo {provider}. Vuoi sovrascriverlo?",
"overwrite": "Sovrascrivi"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Passa a non-custodial",
+ "explainerTitle": "Cosa significa passare a non-custodial?",
+ "explainerStep1": "Creerai un account non-custodial sul protocollo Spark, scopri di più qui",
+ "explainerStep2": "Trasferiamo i tuoi fondi nel tuo nuovo account non-custodial e il tuo account attuale verrà eliminato",
+ "explainerStep3": "Continua a usare Blink come al solito",
+ "letsMove": "Andiamo",
+ "transferringFunds": "Trasferimento dei tuoi fondi in corso. Dovrebbe completarsi in pochi secondi."
}
}
diff --git a/app/i18n/raw-i18n/translations/ja.json b/app/i18n/raw-i18n/translations/ja.json
index 7aa2925e37..f8db5fc516 100644
--- a/app/i18n/raw-i18n/translations/ja.json
+++ b/app/i18n/raw-i18n/translations/ja.json
@@ -3522,5 +3522,14 @@
"existingBackupMessage": "{provider}にバックアップが既に存在します。上書きしますか?",
"overwrite": "上書き"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "ノンカストディアルに移行",
+ "explainerTitle": "ノンカストディアルへの移行とは?",
+ "explainerStep1": "Sparkプロトコル上にノンカストディアルアカウントを作成します、詳細はこちら",
+ "explainerStep2": "お客様の資金を新しいノンカストディアルアカウントに転送し、現在のアカウントは削除されます",
+ "explainerStep3": "これまで通りBlinkをご利用ください",
+ "letsMove": "移行する",
+ "transferringFunds": "資金を転送中です。数秒で完了するはずです。"
}
}
diff --git a/app/i18n/raw-i18n/translations/lg.json b/app/i18n/raw-i18n/translations/lg.json
index 009b46f1fa..f51d9c0c42 100644
--- a/app/i18n/raw-i18n/translations/lg.json
+++ b/app/i18n/raw-i18n/translations/lg.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Waliwo kopi mu {provider} yo. Oyagala okugisimbulira?",
"overwrite": "Simbulira"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Tuuka ku non-custodial",
+ "explainerTitle": "Kitegeeza ki okutuuka ku non-custodial?",
+ "explainerStep1": "Ojja okukola akawunti ya non-custodial ku Spark protocol, yiga ebisingawo wano",
+ "explainerStep2": "Tutwala ssente zo ku akawunti yo empya eya non-custodial n'akawunti yo eriwo ejja kusazibwamu",
+ "explainerStep3": "Weyongere okukozesa Blink nga bulijjo",
+ "letsMove": "Ka tugende",
+ "transferringFunds": "Tutwala ssente zo. Kijja kuggwa mu sikonda ntono."
}
}
diff --git a/app/i18n/raw-i18n/translations/ms.json b/app/i18n/raw-i18n/translations/ms.json
index 11a49f554c..162a4c484f 100644
--- a/app/i18n/raw-i18n/translations/ms.json
+++ b/app/i18n/raw-i18n/translations/ms.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "Sandaran sudah wujud di {provider} anda. Adakah anda mahu menimpanya?",
"overwrite": "Tulis ganti"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Beralih ke non-custodial",
+ "explainerTitle": "Apakah maksud beralih ke non-custodial?",
+ "explainerStep1": "Anda akan membuat akaun non-custodial di protokol Spark, ketahui lebih lanjut di sini",
+ "explainerStep2": "Kami memindahkan dana anda ke akaun non-custodial baharu dan akaun semasa anda akan dipadamkan",
+ "explainerStep3": "Teruskan menggunakan Blink seperti biasa",
+ "letsMove": "Jom",
+ "transferringFunds": "Memindahkan dana anda. Ia sepatutnya siap dalam beberapa saat."
}
}
diff --git a/app/i18n/raw-i18n/translations/nl.json b/app/i18n/raw-i18n/translations/nl.json
index cdb3f7ab5e..a01e213186 100644
--- a/app/i18n/raw-i18n/translations/nl.json
+++ b/app/i18n/raw-i18n/translations/nl.json
@@ -3534,5 +3534,14 @@
"existingBackupMessage": "Er bestaat al een back-up in je {provider}. Wil je deze overschrijven?",
"overwrite": "Overschrijven"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Overstappen naar non-custodial",
+ "explainerTitle": "Wat betekent overstappen naar non-custodial?",
+ "explainerStep1": "Je maakt een non-custodial account aan op het Spark-protocol, lees hier meer",
+ "explainerStep2": "We dragen je saldo over naar je nieuwe non-custodial account en je huidige account wordt verwijderd",
+ "explainerStep3": "Blijf Blink gebruiken zoals gewoonlijk",
+ "letsMove": "Laten we gaan",
+ "transferringFunds": "Je saldo wordt overgedragen. Het zou binnen enkele seconden klaar moeten zijn."
}
}
diff --git a/app/i18n/raw-i18n/translations/pt.json b/app/i18n/raw-i18n/translations/pt.json
index 8886cc72f0..3c08dadb9f 100644
--- a/app/i18n/raw-i18n/translations/pt.json
+++ b/app/i18n/raw-i18n/translations/pt.json
@@ -3481,5 +3481,14 @@
"existingBackupMessage": "Já existe um backup no seu {provider}. Deseja sobrescrevê-lo?",
"overwrite": "Sobrescrever"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Migrar para não custodial",
+ "explainerTitle": "O que significa migrar para não custodial?",
+ "explainerStep1": "Você criará uma conta não custodial no protocolo Spark, saiba mais aqui",
+ "explainerStep2": "Transferiremos seus fundos para sua nova conta não custodial e sua conta atual será excluída",
+ "explainerStep3": "Continue usando o Blink como de costume",
+ "letsMove": "Vamos lá",
+ "transferringFunds": "Transferindo seus fundos. Deve ser concluído em poucos segundos."
}
}
diff --git a/app/i18n/raw-i18n/translations/qu.json b/app/i18n/raw-i18n/translations/qu.json
index 2aa206b123..1d671a7dda 100644
--- a/app/i18n/raw-i18n/translations/qu.json
+++ b/app/i18n/raw-i18n/translations/qu.json
@@ -3531,5 +3531,14 @@
"existingBackupMessage": "{provider} nisqaykipi huknin waqaychay kachkanña. Qawariyta munankichu?",
"overwrite": "Hawamanta qillqay"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Non-custodial nisqaman kutiy",
+ "explainerTitle": "Imataq niyta munan non-custodial nisqaman kutiy?",
+ "explainerStep1": "Spark protocol nisqapi non-custodial akawuntita ruwanki, astawanta yachay kaypi",
+ "explainerStep2": "Qullqiykita musuq non-custodial akawuntiykiman apasun, kunan akawuntiykitaq puchukanqa",
+ "explainerStep3": "Blink nisqata llamkachiy imaynallapas",
+ "letsMove": "Haku",
+ "transferringFunds": "Qullqiykita apachkaniku. Iskay kinsa sikundullapi tukurunqa."
}
}
diff --git a/app/i18n/raw-i18n/translations/ro.json b/app/i18n/raw-i18n/translations/ro.json
index b3c9fcd328..205fa3ada6 100644
--- a/app/i18n/raw-i18n/translations/ro.json
+++ b/app/i18n/raw-i18n/translations/ro.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Există deja o copie de siguranță în {provider} tău. Vrei să o suprascrii?",
"overwrite": "Suprascrie"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Treci la non-custodial",
+ "explainerTitle": "Ce înseamnă să treci la non-custodial?",
+ "explainerStep1": "Vei crea un cont non-custodial pe protocolul Spark, află mai multe aici",
+ "explainerStep2": "Transferăm fondurile tale în noul tău cont non-custodial, iar contul actual va fi șters",
+ "explainerStep3": "Continuă să folosești Blink ca de obicei",
+ "letsMove": "Hai să mergem",
+ "transferringFunds": "Se transferă fondurile tale. Ar trebui să fie gata în câteva secunde."
}
}
diff --git a/app/i18n/raw-i18n/translations/sk.json b/app/i18n/raw-i18n/translations/sk.json
index 7e457af957..85f8c02f97 100644
--- a/app/i18n/raw-i18n/translations/sk.json
+++ b/app/i18n/raw-i18n/translations/sk.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Vo vašom {provider} už existuje záloha. Chcete ju prepísať?",
"overwrite": "Prepísať"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Prejsť na non-custodial",
+ "explainerTitle": "Čo znamená prejsť na non-custodial?",
+ "explainerStep1": "Vytvoríte non-custodial účet na protokole Spark, zistiť viac tu",
+ "explainerStep2": "Prevedieme vaše prostriedky na váš nový non-custodial účet a váš súčasný účet bude vymazaný",
+ "explainerStep3": "Pokračujte v používaní Blink ako zvyčajne",
+ "letsMove": "Poďme na to",
+ "transferringFunds": "Prevádzame vaše prostriedky. Malo by to byť hotové za niekoľko sekúnd."
}
}
diff --git a/app/i18n/raw-i18n/translations/sr.json b/app/i18n/raw-i18n/translations/sr.json
index 0ac10c9c25..740e8b316d 100644
--- a/app/i18n/raw-i18n/translations/sr.json
+++ b/app/i18n/raw-i18n/translations/sr.json
@@ -3531,5 +3531,14 @@
"existingBackupMessage": "Резервна копија већ постоји у вашем {provider}. Желите ли да је препишете?",
"overwrite": "Препиши"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Пребаци на non-custodial",
+ "explainerTitle": "Шта значи пребацити на non-custodial?",
+ "explainerStep1": "Креираћете non-custodial налог на Spark протоколу, сазнајте више овде",
+ "explainerStep2": "Пребацујемо ваша средства на ваш нови non-custodial налог, а ваш тренутни налог ће бити обрисан",
+ "explainerStep3": "Наставите да користите Blink као и обично",
+ "letsMove": "Идемо",
+ "transferringFunds": "Пребацујемо ваша средства. Требало би да буде готово за неколико секунди."
}
}
diff --git a/app/i18n/raw-i18n/translations/sw.json b/app/i18n/raw-i18n/translations/sw.json
index 5cb562877e..9d4a222355 100644
--- a/app/i18n/raw-i18n/translations/sw.json
+++ b/app/i18n/raw-i18n/translations/sw.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "Tayari kuna nakala kwenye {provider} yako. Je, ungependa kuibadilisha?",
"overwrite": "Badilisha"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Hamia kwa non-custodial",
+ "explainerTitle": "Inamaanisha nini kuhamia kwa non-custodial?",
+ "explainerStep1": "Utaunda akaunti ya non-custodial kwenye itifaki ya Spark, jifunze zaidi hapa",
+ "explainerStep2": "Tunahamisha fedha zako kwenye akaunti yako mpya ya non-custodial na akaunti yako ya sasa itafutwa",
+ "explainerStep3": "Endelea kutumia Blink kama kawaida",
+ "letsMove": "Twende",
+ "transferringFunds": "Tunahamisha fedha zako. Inapaswa kukamilika ndani ya sekunde chache."
}
}
diff --git a/app/i18n/raw-i18n/translations/th.json b/app/i18n/raw-i18n/translations/th.json
index 439b74156f..49a56fb9a0 100644
--- a/app/i18n/raw-i18n/translations/th.json
+++ b/app/i18n/raw-i18n/translations/th.json
@@ -3531,5 +3531,14 @@
"existingBackupMessage": "มีข้อมูลสำรองอยู่แล้วใน {provider} ของคุณ คุณต้องการเขียนทับหรือไม่?",
"overwrite": "เขียนทับ"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "ย้ายไปแบบ non-custodial",
+ "explainerTitle": "การย้ายไปแบบ non-custodial หมายความว่าอย่างไร?",
+ "explainerStep1": "คุณจะสร้างบัญชีแบบ non-custodial บนโปรโตคอล Spark เรียนรู้เพิ่มเติมที่นี่",
+ "explainerStep2": "เราจะโอนเงินของคุณไปยังบัญชี non-custodial ใหม่และบัญชีปัจจุบันของคุณจะถูกลบ",
+ "explainerStep3": "ใช้ Blink ต่อไปได้ตามปกติ",
+ "letsMove": "ไปกันเลย",
+ "transferringFunds": "กำลังโอนเงินของคุณ จะเสร็จสิ้นภายในไม่กี่วินาที"
}
}
diff --git a/app/i18n/raw-i18n/translations/tr.json b/app/i18n/raw-i18n/translations/tr.json
index fd6b87c3d2..b72e64f3cc 100644
--- a/app/i18n/raw-i18n/translations/tr.json
+++ b/app/i18n/raw-i18n/translations/tr.json
@@ -3493,5 +3493,14 @@
"existingBackupMessage": "{provider} hesabınızda zaten bir yedek var. Üzerine yazmak ister misiniz?",
"overwrite": "Üzerine yaz"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Non-custodial'a geç",
+ "explainerTitle": "Non-custodial'a geçmek ne anlama gelir?",
+ "explainerStep1": "Spark protokolünde bir non-custodial hesap oluşturacaksınız, buradan daha fazla bilgi edinin",
+ "explainerStep2": "Bakiyenizi yeni non-custodial hesabınıza aktarıyoruz ve mevcut hesabınız silinecek",
+ "explainerStep3": "Blink'i her zamanki gibi kullanmaya devam edin",
+ "letsMove": "Hadi başlayalım",
+ "transferringFunds": "Bakiyeniz aktarılıyor. Birkaç saniye içinde tamamlanacaktır."
}
}
diff --git a/app/i18n/raw-i18n/translations/vi.json b/app/i18n/raw-i18n/translations/vi.json
index a452fecd51..aae5a9a0c2 100644
--- a/app/i18n/raw-i18n/translations/vi.json
+++ b/app/i18n/raw-i18n/translations/vi.json
@@ -3531,5 +3531,14 @@
"existingBackupMessage": "Đã có bản sao lưu trong {provider} của bạn. Bạn có muốn ghi đè không?",
"overwrite": "Ghi đè"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Chuyển sang non-custodial",
+ "explainerTitle": "Chuyển sang non-custodial nghĩa là gì?",
+ "explainerStep1": "Bạn sẽ tạo một tài khoản non-custodial trên giao thức Spark, tìm hiểu thêm tại đây",
+ "explainerStep2": "Chúng tôi chuyển tiền của bạn vào tài khoản non-custodial mới và tài khoản hiện tại của bạn sẽ bị xóa",
+ "explainerStep3": "Tiếp tục sử dụng Blink như bình thường",
+ "letsMove": "Bắt đầu nào",
+ "transferringFunds": "Đang chuyển tiền của bạn. Sẽ hoàn tất trong vài giây."
}
}
diff --git a/app/i18n/raw-i18n/translations/xh.json b/app/i18n/raw-i18n/translations/xh.json
index 8e772a0b38..ddfd6249ba 100644
--- a/app/i18n/raw-i18n/translations/xh.json
+++ b/app/i18n/raw-i18n/translations/xh.json
@@ -3540,5 +3540,14 @@
"existingBackupMessage": "Sele kukho ikopi kwi-{provider} yakho. Ungathanda ukuyibhala phezu kwayo?",
"overwrite": "Bhala phezu kwayo"
}
+ },
+ "AccountMigration": {
+ "moveToNonCustodial": "Fudukela kwi-non-custodial",
+ "explainerTitle": "Kuthetha ntoni ukufudukela kwi-non-custodial?",
+ "explainerStep1": "Uza kudala i-akhawunti ye-non-custodial kwi-Spark protocol, funda ngakumbi apha",
+ "explainerStep2": "Sidlulisela imali yakho kwi-akhawunti yakho entsha ye-non-custodial kwaye i-akhawunti yakho yangoku iya kucinywa",
+ "explainerStep3": "Qhubeka usebenzise i-Blink njengoko uqhele",
+ "letsMove": "Masiqale",
+ "transferringFunds": "Sidlulisela imali yakho. Kufanele kugqitywe ngomzuzwana nje."
}
}
diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx
index 6ed2ff020d..6873438f01 100644
--- a/app/navigation/root-navigator.tsx
+++ b/app/navigation/root-navigator.tsx
@@ -130,6 +130,10 @@ import {
SparkBackupConfirmScreen,
SparkBackupSuccessScreen,
} from "@app/screens/spark-onboarding"
+import {
+ SparkMigrationExplainerScreen,
+ TransferringFundsScreen,
+} from "@app/screens/account-migration"
import {
OnboardingStackParamList,
PeopleStackParamList,
@@ -732,6 +736,16 @@ export const RootStack = () => {
component={SparkBackupSuccessScreen}
options={{ headerShown: false }}
/>
+
+
)
}
diff --git a/app/navigation/stack-param-lists.ts b/app/navigation/stack-param-lists.ts
index b12c9419fa..3821e39562 100644
--- a/app/navigation/stack-param-lists.ts
+++ b/app/navigation/stack-param-lists.ts
@@ -187,6 +187,8 @@ export type RootStackParamList = {
challenges: Array<{ index: number; word: string }>
}
sparkBackupSuccessScreen: undefined
+ sparkMigrationExplainer: undefined
+ sparkMigrationTransferringFunds: undefined
}
export type OnboardingStackParamList = {
diff --git a/app/rne-theme/colors.ts b/app/rne-theme/colors.ts
index d19f007d7a..57da1a6598 100644
--- a/app/rne-theme/colors.ts
+++ b/app/rne-theme/colors.ts
@@ -13,6 +13,7 @@ const light = {
_green: "#00A700",
_primary1: "#FFBE0B",
_primary2: "#FB5607",
+ _warningLight: "#FFF9E5",
// adjusted
white: "#FFFFFF",
@@ -64,6 +65,7 @@ const dark = {
_green: "#00A700",
_primary1: "#FFBE0B",
_primary2: "#FB5607",
+ _warningLight: "#FFF9E5",
// adjusted
white: "#000000",
diff --git a/app/rne-theme/themed.d.ts b/app/rne-theme/themed.d.ts
index 953042af47..73af85cd4f 100644
--- a/app/rne-theme/themed.d.ts
+++ b/app/rne-theme/themed.d.ts
@@ -22,6 +22,7 @@ declare module "@rn-vui/themed" {
_green: string
_primary1: string
_primary2: string
+ _warningLight: string
primary3: string
primary4: string
diff --git a/app/screens/account-migration/hooks/index.ts b/app/screens/account-migration/hooks/index.ts
new file mode 100644
index 0000000000..9696993ad8
--- /dev/null
+++ b/app/screens/account-migration/hooks/index.ts
@@ -0,0 +1 @@
+export { useMigrationCheckpoint, MigrationCheckpoint } from "./use-migration-checkpoint"
diff --git a/app/screens/account-migration/hooks/use-migration-checkpoint.ts b/app/screens/account-migration/hooks/use-migration-checkpoint.ts
new file mode 100644
index 0000000000..56733df224
--- /dev/null
+++ b/app/screens/account-migration/hooks/use-migration-checkpoint.ts
@@ -0,0 +1,62 @@
+import { useCallback, useEffect, useRef, useState } from "react"
+
+import { useAppConfig } from "@app/hooks/use-app-config"
+
+import {
+ MigrationCheckpoint,
+ clearCheckpointFromStorage,
+ getStorageKey,
+ loadCheckpoint,
+ resolveCheckpointRoute,
+ saveCheckpointToStorage,
+} from "../utils/migration-checkpoint-storage"
+
+export { MigrationCheckpoint }
+
+export const useMigrationCheckpoint = () => {
+ const [checkpoint, setCheckpoint] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const mountedRef = useRef(true)
+
+ const {
+ appConfig: {
+ galoyInstance: { name: environment },
+ },
+ } = useAppConfig()
+
+ const storageKey = getStorageKey(environment)
+
+ useEffect(() => {
+ mountedRef.current = true
+
+ loadCheckpoint(storageKey).then((stored) => {
+ if (!mountedRef.current) return
+ if (stored) setCheckpoint(stored.step)
+ setLoading(false)
+ })
+
+ return () => {
+ mountedRef.current = false
+ }
+ }, [storageKey])
+
+ const saveCheckpoint = useCallback(
+ (step: MigrationCheckpoint) => {
+ setCheckpoint(step)
+ saveCheckpointToStorage(storageKey, step)
+ },
+ [storageKey],
+ )
+
+ const clearCheckpoint = useCallback(() => {
+ setCheckpoint(null)
+ clearCheckpointFromStorage(storageKey)
+ }, [storageKey])
+
+ const getRouteForCheckpoint = useCallback(
+ () => resolveCheckpointRoute(checkpoint),
+ [checkpoint],
+ )
+
+ return { checkpoint, loading, saveCheckpoint, clearCheckpoint, getRouteForCheckpoint }
+}
diff --git a/app/screens/account-migration/index.ts b/app/screens/account-migration/index.ts
new file mode 100644
index 0000000000..54821267af
--- /dev/null
+++ b/app/screens/account-migration/index.ts
@@ -0,0 +1,3 @@
+export { MigrationExplainerLayout } from "./migration-explainer-layout"
+export { SparkMigrationExplainerScreen } from "./to-non-custodial/explainer-screen"
+export { TransferringFundsScreen } from "./to-non-custodial/transferring-funds-screen"
diff --git a/app/screens/account-migration/migration-explainer-layout.tsx b/app/screens/account-migration/migration-explainer-layout.tsx
new file mode 100644
index 0000000000..67e590631b
--- /dev/null
+++ b/app/screens/account-migration/migration-explainer-layout.tsx
@@ -0,0 +1,53 @@
+import React from "react"
+import { ScrollView, View } from "react-native"
+import { makeStyles } from "@rn-vui/themed"
+
+import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button"
+import { IconNamesType } from "@app/components/atomic/galoy-icon"
+import { IconHero } from "@app/components/icon-hero"
+import { NumberedStepsList } from "@app/components/numbered-steps-list"
+import { Screen } from "@app/components/screen"
+
+type MigrationExplainerLayoutProps = {
+ icon: IconNamesType
+ iconColor: string
+ title: string
+ steps: ReadonlyArray
+ ctaTitle: string
+ onCtaPress: () => void
+}
+
+export const MigrationExplainerLayout: React.FC = ({
+ icon,
+ iconColor,
+ title,
+ steps,
+ ctaTitle,
+ onCtaPress,
+}) => {
+ const styles = useStyles()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const useStyles = makeStyles(() => ({
+ scrollContent: {
+ paddingHorizontal: 20,
+ },
+ buttonsContainer: {
+ paddingHorizontal: 20,
+ paddingBottom: 20,
+ paddingTop: 10,
+ },
+}))
diff --git a/app/screens/account-migration/to-non-custodial/explainer-screen.tsx b/app/screens/account-migration/to-non-custodial/explainer-screen.tsx
new file mode 100644
index 0000000000..7097090fb3
--- /dev/null
+++ b/app/screens/account-migration/to-non-custodial/explainer-screen.tsx
@@ -0,0 +1,47 @@
+import React, { useMemo } from "react"
+import { useNavigation } from "@react-navigation/native"
+import { StackNavigationProp } from "@react-navigation/stack"
+import { useTheme } from "@rn-vui/themed"
+
+import { RichText } from "@app/components/rich-text"
+import { useRemoteConfig } from "@app/config/feature-flags-context"
+import { useI18nContext } from "@app/i18n/i18n-react"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+import { openExternalUrl } from "@app/utils/external"
+
+import { MigrationExplainerLayout } from "../migration-explainer-layout"
+
+export const SparkMigrationExplainerScreen: React.FC = () => {
+ const { LL } = useI18nContext()
+ const {
+ theme: { colors },
+ } = useTheme()
+ const navigation = useNavigation>()
+ const { sparkCompatibleWalletsUrl } = useRemoteConfig()
+
+ const steps: ReadonlyArray = useMemo(
+ () => [
+ openExternalUrl(sparkCompatibleWalletsUrl) },
+ }}
+ />,
+ LL.AccountMigration.explainerStep2(),
+ LL.AccountMigration.explainerStep3(),
+ ],
+ [LL, sparkCompatibleWalletsUrl],
+ )
+
+ return (
+ navigation.navigate("sparkBackupMethodScreen")}
+ />
+ )
+}
diff --git a/app/screens/account-migration/to-non-custodial/transferring-funds-screen.tsx b/app/screens/account-migration/to-non-custodial/transferring-funds-screen.tsx
new file mode 100644
index 0000000000..ad2d0320f3
--- /dev/null
+++ b/app/screens/account-migration/to-non-custodial/transferring-funds-screen.tsx
@@ -0,0 +1,46 @@
+import React, { useEffect } from "react"
+import { Text } from "react-native"
+
+import { useNavigation } from "@react-navigation/native"
+import { StackNavigationProp } from "@react-navigation/stack"
+import { makeStyles, useTheme } from "@rn-vui/themed"
+
+import { Screen } from "@app/components/screen"
+import { StatusScreenLayout } from "@app/components/status-screen-layout"
+import { useI18nContext } from "@app/i18n/i18n-react"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+
+export const TransferringFundsScreen: React.FC = () => {
+ const { LL } = useI18nContext()
+ const styles = useStyles()
+ const {
+ theme: { colors },
+ } = useTheme()
+ const navigation = useNavigation>()
+
+ // TODO: replace with real funds transfer logic
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ navigation.replace("sparkBackupSuccessScreen")
+ }, 2000)
+ return () => clearTimeout(timeout)
+ }, [navigation])
+
+ return (
+
+
+ {LL.AccountMigration.transferringFunds()}
+
+
+ )
+}
+
+const useStyles = makeStyles(({ colors }) => ({
+ message: {
+ fontSize: 18,
+ lineHeight: 24,
+ fontWeight: "400",
+ color: colors.black,
+ textAlign: "center",
+ },
+}))
diff --git a/app/screens/account-migration/utils/migration-checkpoint-storage.ts b/app/screens/account-migration/utils/migration-checkpoint-storage.ts
new file mode 100644
index 0000000000..c24877a362
--- /dev/null
+++ b/app/screens/account-migration/utils/migration-checkpoint-storage.ts
@@ -0,0 +1,96 @@
+import { Platform } from "react-native"
+
+import { loadJson, remove, saveJson } from "@app/utils/storage"
+
+// Values are persisted to AsyncStorage — do not rename
+export enum MigrationCheckpoint {
+ BackupMethod = "backupMethod",
+ CloudBackup = "cloudBackup",
+ BackupAlerts = "backupAlerts",
+}
+
+type StoredCheckpoint = {
+ step: MigrationCheckpoint
+ savedAt: number
+}
+
+type ResumeRoute =
+ | "sparkMigrationExplainer"
+ | "sparkBackupMethodScreen"
+ | "sparkCloudBackupScreen"
+ | "sparkBackupAlertsScreen"
+
+const STORAGE_KEY_PREFIX = "migrationCheckpoint"
+
+const CHECKPOINT_EXPIRATION_MS = 48 * 60 * 60 * 1000 // 48h
+
+const CHECKPOINT_ROUTE_MAP: Record = {
+ [MigrationCheckpoint.BackupMethod]: "sparkBackupMethodScreen",
+ [MigrationCheckpoint.CloudBackup]: "sparkCloudBackupScreen",
+ [MigrationCheckpoint.BackupAlerts]: "sparkBackupAlertsScreen",
+}
+
+const DEFAULT_ROUTE: ResumeRoute = "sparkMigrationExplainer"
+
+export const getStorageKey = (environment: string): string =>
+ `${STORAGE_KEY_PREFIX}_${environment.toLowerCase()}`
+
+export const isExpired = (
+ checkpoint: StoredCheckpoint,
+ now: number = Date.now(),
+): boolean => now - checkpoint.savedAt > CHECKPOINT_EXPIRATION_MS
+
+export const validateStoredCheckpoint = (raw: unknown): StoredCheckpoint | null => {
+ if (!raw || typeof raw !== "object") return null
+
+ const { step, savedAt } = raw as StoredCheckpoint
+
+ if (!Object.values(MigrationCheckpoint).includes(step)) return null
+ if (typeof savedAt !== "number") return null
+
+ return { step, savedAt }
+}
+
+export const resolveCheckpointRoute = (
+ checkpoint: MigrationCheckpoint | null,
+): ResumeRoute => {
+ if (!checkpoint) return DEFAULT_ROUTE
+
+ if (checkpoint === MigrationCheckpoint.CloudBackup && Platform.OS === "ios") {
+ return DEFAULT_ROUTE
+ }
+
+ return CHECKPOINT_ROUTE_MAP[checkpoint]
+}
+
+export const loadCheckpoint = async (
+ storageKey: string,
+): Promise => {
+ try {
+ const raw = await loadJson(storageKey)
+ const parsed = validateStoredCheckpoint(raw)
+
+ if (!parsed) return null
+
+ if (isExpired(parsed)) {
+ await remove(storageKey)
+ return null
+ }
+
+ return parsed
+ } catch {
+ await remove(storageKey).catch(() => {})
+ return null
+ }
+}
+
+export const saveCheckpointToStorage = async (
+ storageKey: string,
+ step: MigrationCheckpoint,
+): Promise => {
+ await saveJson(storageKey, { step, savedAt: Date.now() })
+}
+
+export const clearCheckpointFromStorage = async (storageKey: string): Promise => {
+ await remove(storageKey)
+}
diff --git a/app/screens/settings-screen/settings-screen.tsx b/app/screens/settings-screen/settings-screen.tsx
index badc5d1a8e..1289e7adbe 100644
--- a/app/screens/settings-screen/settings-screen.tsx
+++ b/app/screens/settings-screen/settings-screen.tsx
@@ -36,6 +36,7 @@ import { NotificationSetting } from "./settings/sp-notifications"
import { OnDeviceSecuritySetting } from "./settings/sp-security"
import { TotpSetting } from "./totp"
import { AccountStaticQR } from "./settings/account-static-qr"
+import { MoveToNonCustodialSetting } from "./settings/account-move-to-noncustodial"
import { SwitchAccountSetting } from "./settings/multi-account"
// All queries in settings have to be set here so that the server is not hit with
@@ -84,7 +85,12 @@ export const SettingsScreen: React.FC = () => {
})
const items = {
- account: [AccountLevelSetting, TxLimits, SwitchAccountSetting],
+ account: [
+ AccountLevelSetting,
+ TxLimits,
+ SwitchAccountSetting,
+ MoveToNonCustodialSetting,
+ ],
waysToGetPaid: [AccountLNAddress, PhoneLnAddress, AccountPOS, AccountStaticQR],
loginMethods: [EmailSetting, PhoneSetting],
preferences: [
diff --git a/app/screens/settings-screen/settings/account-move-to-noncustodial.tsx b/app/screens/settings-screen/settings/account-move-to-noncustodial.tsx
new file mode 100644
index 0000000000..452ef73bc3
--- /dev/null
+++ b/app/screens/settings-screen/settings/account-move-to-noncustodial.tsx
@@ -0,0 +1,25 @@
+import React from "react"
+
+import { useNavigation } from "@react-navigation/native"
+import { StackNavigationProp } from "@react-navigation/stack"
+
+import { useI18nContext } from "@app/i18n/i18n-react"
+import { RootStackParamList } from "@app/navigation/stack-param-lists"
+import { useMigrationCheckpoint } from "@app/screens/account-migration/hooks"
+
+import { SettingsRow } from "../row"
+
+export const MoveToNonCustodialSetting: React.FC = () => {
+ const { LL } = useI18nContext()
+ const { navigate } = useNavigation>()
+ const { loading, getRouteForCheckpoint } = useMigrationCheckpoint()
+
+ return (
+ navigate(getRouteForCheckpoint())}
+ />
+ )
+}
diff --git a/app/screens/spark-onboarding/backup-method-screen.tsx b/app/screens/spark-onboarding/backup-method-screen.tsx
index eed972dab4..8bf40dc70d 100644
--- a/app/screens/spark-onboarding/backup-method-screen.tsx
+++ b/app/screens/spark-onboarding/backup-method-screen.tsx
@@ -1,4 +1,4 @@
-import React from "react"
+import React, { useEffect } from "react"
import { View } from "react-native"
import { Text, makeStyles, useTheme } from "@rn-vui/themed"
@@ -8,6 +8,10 @@ import { GaloySecondaryButton } from "@app/components/atomic/galoy-secondary-but
import { IconHero } from "@app/components/icon-hero"
import { Screen } from "@app/components/screen"
import { useI18nContext } from "@app/i18n/i18n-react"
+import {
+ MigrationCheckpoint,
+ useMigrationCheckpoint,
+} from "@app/screens/account-migration/hooks"
import { testProps } from "@app/utils/testProps"
import { useBackupMethods } from "./hooks"
@@ -20,6 +24,7 @@ export const SparkBackupMethodScreen: React.FC = () => {
theme: { colors },
} = useTheme()
+ const { saveCheckpoint } = useMigrationCheckpoint()
const {
isCloudBackupAvailable,
keychainLoading,
@@ -28,6 +33,10 @@ export const SparkBackupMethodScreen: React.FC = () => {
handleManualBackup,
} = useBackupMethods()
+ useEffect(() => {
+ saveCheckpoint(MigrationCheckpoint.BackupMethod)
+ }, [saveCheckpoint])
+
const cloudProvider = getCloudProviderName(LL)
return (
diff --git a/app/screens/spark-onboarding/backup-success-screen.tsx b/app/screens/spark-onboarding/backup-success-screen.tsx
index 3529665b0d..628af45251 100644
--- a/app/screens/spark-onboarding/backup-success-screen.tsx
+++ b/app/screens/spark-onboarding/backup-success-screen.tsx
@@ -6,15 +6,18 @@ import { CommonActions, useNavigation } from "@react-navigation/native"
import { Screen } from "@app/components/screen"
import { SuccessScreenLayout } from "@app/components/success-screen-layout"
import { useI18nContext } from "@app/i18n/i18n-react"
+import { useMigrationCheckpoint } from "@app/screens/account-migration/hooks"
export const SparkBackupSuccessScreen: React.FC = () => {
const { LL } = useI18nContext()
const styles = useStyles()
const navigation = useNavigation()
+ const { clearCheckpoint } = useMigrationCheckpoint()
const navigateToHome = useCallback(() => {
+ clearCheckpoint()
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: "Primary" }] }))
- }, [navigation])
+ }, [navigation, clearCheckpoint])
return (
diff --git a/app/screens/spark-onboarding/cloud-backup-screen.tsx b/app/screens/spark-onboarding/cloud-backup-screen.tsx
index e2f410fd68..d6294d7643 100644
--- a/app/screens/spark-onboarding/cloud-backup-screen.tsx
+++ b/app/screens/spark-onboarding/cloud-backup-screen.tsx
@@ -89,9 +89,8 @@ export const SparkCloudBackupScreen: React.FC = () => {
>
${LL.SparkOnboarding.CloudBackup.importantMessageBold()}`,
})}
- bold={LL.SparkOnboarding.CloudBackup.importantMessageBold()}
/>
diff --git a/app/screens/spark-onboarding/hooks/use-backup-phrase.ts b/app/screens/spark-onboarding/hooks/use-backup-phrase.ts
index de99dd0132..4560e1dc27 100644
--- a/app/screens/spark-onboarding/hooks/use-backup-phrase.ts
+++ b/app/screens/spark-onboarding/hooks/use-backup-phrase.ts
@@ -1,9 +1,7 @@
import { useCallback, useMemo, useRef } from "react"
-import { Linking } from "react-native"
import { useNavigation } from "@react-navigation/native"
import { StackNavigationProp } from "@react-navigation/stack"
-import InAppBrowser from "react-native-inappbrowser-reborn"
import { useRemoteConfig } from "@app/config/feature-flags-context"
import { useClipboard, useCountdown } from "@app/hooks"
@@ -11,6 +9,7 @@ import { useWalletMnemonicWords } from "@app/hooks/use-wallet-mnemonic"
import { useI18nContext } from "@app/i18n/i18n-react"
import { PhraseStep, RootStackParamList } from "@app/navigation/stack-param-lists"
import { formatDuration } from "@app/utils/date"
+import { openExternalUrl } from "@app/utils/external"
import { buildConfirmChallenges } from "../utils"
@@ -49,10 +48,7 @@ export const useBackupPhrase = (step: PhraseStep) => {
}, [copyToClipboard, LL, words])
const handleOpenLink = useCallback(
- () =>
- InAppBrowser.open(sparkCompatibleWalletsUrl).catch(() =>
- Linking.openURL(sparkCompatibleWalletsUrl),
- ),
+ () => openExternalUrl(sparkCompatibleWalletsUrl),
[sparkCompatibleWalletsUrl],
)
diff --git a/app/screens/spark-onboarding/manual-backup/backup-alerts-screen.tsx b/app/screens/spark-onboarding/manual-backup/backup-alerts-screen.tsx
index 774f3d87fb..8365571a22 100644
--- a/app/screens/spark-onboarding/manual-backup/backup-alerts-screen.tsx
+++ b/app/screens/spark-onboarding/manual-backup/backup-alerts-screen.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useState } from "react"
+import React, { useEffect, useRef, useState } from "react"
import { Animated, View } from "react-native"
import { makeStyles, useTheme } from "@rn-vui/themed"
@@ -11,6 +11,10 @@ import { IconHero } from "@app/components/icon-hero"
import { Screen } from "@app/components/screen"
import { useI18nContext } from "@app/i18n/i18n-react"
import { PhraseStep, RootStackParamList } from "@app/navigation/stack-param-lists"
+import {
+ MigrationCheckpoint,
+ useMigrationCheckpoint,
+} from "@app/screens/account-migration/hooks"
const ANIM_DURATION = 300
@@ -21,6 +25,11 @@ export const SparkBackupAlertsScreen: React.FC = () => {
theme: { colors },
} = useTheme()
const navigation = useNavigation>()
+ const { saveCheckpoint } = useMigrationCheckpoint()
+
+ useEffect(() => {
+ saveCheckpoint(MigrationCheckpoint.BackupAlerts)
+ }, [saveCheckpoint])
const [checks, setChecks] = useState([false, false, false])
const [visibleCount, setVisibleCount] = useState(1)
diff --git a/app/screens/spark-onboarding/manual-backup/backup-confirm-screen.tsx b/app/screens/spark-onboarding/manual-backup/backup-confirm-screen.tsx
index 226b5a0e6a..420cf02eda 100644
--- a/app/screens/spark-onboarding/manual-backup/backup-confirm-screen.tsx
+++ b/app/screens/spark-onboarding/manual-backup/backup-confirm-screen.tsx
@@ -9,6 +9,7 @@ import { GaloyIcon } from "@app/components/atomic/galoy-icon"
import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button"
import { Screen } from "@app/components/screen"
import { SuggestionBar } from "@app/components/suggestion-bar"
+import { useHomeAuthedQuery } from "@app/graphql/generated"
import { useI18nContext } from "@app/i18n/i18n-react"
import { RootStackParamList } from "@app/navigation/stack-param-lists"
@@ -25,9 +26,17 @@ export const SparkBackupConfirmScreen: React.FC = () => {
const navigation = useNavigation>()
const { challenges } = useRoute().params
+ const { data: { me } = {} } = useHomeAuthedQuery({ fetchPolicy: "cache-first" })
+ const hasFunds =
+ me?.defaultAccount?.wallets?.some((wallet) => wallet.balance > 0) ?? false
+
const onComplete = useCallback(() => {
+ if (hasFunds) {
+ navigation.navigate("sparkMigrationTransferringFunds")
+ return
+ }
navigation.navigate("sparkBackupSuccessScreen")
- }, [navigation])
+ }, [navigation, hasFunds])
const {
inputs,
diff --git a/app/utils/external.ts b/app/utils/external.ts
index 0ef5c37de9..b4fe88d261 100644
--- a/app/utils/external.ts
+++ b/app/utils/external.ts
@@ -1,4 +1,5 @@
import { Linking } from "react-native"
+import InAppBrowser from "react-native-inappbrowser-reborn"
export const openWhatsApp: (number: string, message: string) => Promise = async (
number: string,
@@ -9,3 +10,11 @@ export const openWhatsApp: (number: string, message: string) => Promise =
message,
)}`,
)
+
+export const openExternalUrl = async (url: string): Promise => {
+ try {
+ await InAppBrowser.open(url)
+ } catch {
+ await Linking.openURL(url)
+ }
+}
diff --git a/jest.config.js b/jest.config.js
index 24a67d6194..327997f492 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -54,6 +54,7 @@ module.exports = {
"|react-native-nfc-manager" +
"|uuid" +
"|@formatjs" +
+ "|react-native-inappbrowser-reborn" +
")/)",
],
}