diff --git a/app/index.tsx b/app/index.tsx
index e4dbbae..c3e8773 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,8 +1,23 @@
+import { useEffect } from "react";
import { Redirect } from "expo-router";
+import { View, ActivityIndicator } from "react-native";
import { useWalletStore } from "@/store/walletStore";
export default function Index() {
- const { isOnboarded } = useWalletStore();
+ const { isOnboarded, hydrated, hydrate } = useWalletStore();
+
+ useEffect(() => {
+ if (!hydrated) hydrate();
+ }, [hydrated, hydrate]);
+
+ if (!hydrated) {
+ return (
+
+
+
+ );
+ }
+
return isOnboarded ? (
) : (
diff --git a/package-lock.json b/package-lock.json
index e38e4c1..670ec89 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"expo-font": "~11.10.3",
"expo-linking": "~6.2.2",
"expo-router": "~3.4.0",
+ "expo-screen-capture": "~6.0.1",
"expo-secure-store": "~12.8.1",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
@@ -73,6 +74,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -2045,6 +2047,7 @@
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz",
"integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/compat-data": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
@@ -5618,7 +5621,6 @@
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.0.tgz",
"integrity": "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/traverse": "^7.29.0",
"@react-native/codegen": "0.86.0"
@@ -5680,7 +5682,6 @@
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.0.tgz",
"integrity": "sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/parser": "^7.29.0",
@@ -6161,6 +6162,7 @@
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"deprecated": "This version is no longer supported",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@react-navigation/core": "^6.4.17",
"escape-string-regexp": "^4.0.0",
@@ -6540,6 +6542,7 @@
"integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6646,6 +6649,7 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -6922,6 +6926,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7542,7 +7547,6 @@
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.0.tgz",
"integrity": "sha512-LhD0xdoedDw7ansQgXbB2DADLZIK/LRXuWNBPuVzMc5S2WK5GyT89tCM+cQzxFGO0mGyLK6D5TrVOJJzAoDy8Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hermes-parser": "0.36.0"
}
@@ -8015,6 +8019,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.42",
"caniuse-lite": "^1.0.30001803",
@@ -9812,6 +9817,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -10218,6 +10224,7 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-50.0.21.tgz",
"integrity": "sha512-lY+HJdQcsTUbEtPhgT3Y2+WwKZdJiYN0Zq5yAOT9293N1TbdLbHCNkOUtFfTmK0JjwgSKbbH4kRlue7a4MJflg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.17.13",
@@ -10279,7 +10286,6 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.6.tgz",
"integrity": "sha512-OV+4XUshdO18TKNlo1cxUkXeJWgUOPgalvl8ofmc7kmPPHoyfz2hGJ94tyY/RND/GG5RREE+me9YHClNEzo+Ow==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@expo/env": "~2.4.2"
},
@@ -10293,7 +10299,6 @@
"resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz",
"integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"chalk": "^4.0.0",
"debug": "^4.3.4",
@@ -10308,7 +10313,6 @@
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
"integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -10339,6 +10343,7 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-11.10.3.tgz",
"integrity": "sha512-q1Td2zUvmLbCA9GV4OG4nLPw5gJuNY1VrPycsnemN1m8XWTzzs8nyECQQqrcBhgulCgcKZZJJ6U0kC2iuSoQHQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -10513,6 +10518,15 @@
"react-native-screens": ">= 3.0.0"
}
},
+ "node_modules/expo-screen-capture": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/expo-screen-capture/-/expo-screen-capture-6.0.1.tgz",
+ "integrity": "sha512-rJnwcKxZ6anfEx2xFeRo9MAVp2JgI4S+TuhUurHRf9rildPVorS2JvLey3i5xZPs7CxEe2xcijTmr5WT5Lw4dA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-secure-store": {
"version": "12.8.1",
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-12.8.1.tgz",
@@ -10538,7 +10552,8 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.11.1.tgz",
"integrity": "sha512-ddQEtCOgYHTLlFUe/yH67dDBIoct5VIULthyT3LRJbEwdpzAgueKsX2FYK02ldh440V87PWKCamh7R9evk1rrg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/exponential-backoff": {
"version": "3.1.3",
@@ -10681,7 +10696,6 @@
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -11350,6 +11364,7 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz",
"integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">= 10.x"
}
@@ -11460,15 +11475,13 @@
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz",
"integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/hermes-parser": {
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz",
"integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"hermes-estree": "0.36.0"
}
@@ -12524,6 +12537,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -13857,9 +13871,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -13880,9 +13891,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -13903,9 +13911,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -13926,9 +13931,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16215,6 +16217,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -16282,6 +16285,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz",
"integrity": "sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native-community/cli": "12.3.2",
@@ -16351,6 +16355,7 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.8.2.tgz",
"integrity": "sha512-ffUOv8BJQ6RqO3nLml5gxJ6ab3EestPiyWekxdzO/1MQ7NF8fW1Mzh1C5QE9yq573Xefnc7FuzGXjtesZGv7cQ==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -16361,6 +16366,7 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.29.0.tgz",
"integrity": "sha512-yB1GoAMamFAcYf4ku94uBPn0/ani9QG7NdI98beJ5cet2YFESYYzuEIuU+kt+CNRcO8qqKeugxlfgAa3HyTqlg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
@@ -17022,6 +17028,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -18371,7 +18378,6 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
@@ -18388,7 +18394,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -18686,6 +18691,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/src/store/__tests__/walletStore.test.ts b/src/store/__tests__/walletStore.test.ts
new file mode 100644
index 0000000..747562c
--- /dev/null
+++ b/src/store/__tests__/walletStore.test.ts
@@ -0,0 +1,34 @@
+jest.mock("@/services/secureStorage", () => ({
+ getSecretKey: jest.fn(),
+}));
+
+import { getSecretKey } from "@/services/secureStorage";
+import { useWalletStore } from "@/store/walletStore";
+
+const mockedGetSecretKey = getSecretKey as jest.Mock;
+
+beforeEach(() => {
+ useWalletStore.setState({ isOnboarded: false, publicKey: null, balances: {}, hydrated: false });
+ mockedGetSecretKey.mockReset();
+});
+
+describe("walletStore", () => {
+ it("restores onboarded state when a secret exists in SecureStore", async () => {
+ mockedGetSecretKey.mockResolvedValue("SBGELNIUVE6A5I43FCPBKX7ZIO3AO63XPWP5WEQBOE4F6NTK6RNKIG3X");
+ await useWalletStore.getState().hydrate();
+ const state = useWalletStore.getState();
+ expect(state.hydrated).toBe(true);
+ expect(state.isOnboarded).toBe(true);
+ expect(state.publicKey).toBeTruthy();
+ expect(state.publicKey).toMatch(/^G[A-Z0-9]{55}$/);
+ });
+
+ it("leaves isOnboarded false when no secret exists", async () => {
+ mockedGetSecretKey.mockResolvedValue(null);
+ await useWalletStore.getState().hydrate();
+ const state = useWalletStore.getState();
+ expect(state.hydrated).toBe(true);
+ expect(state.isOnboarded).toBe(false);
+ expect(state.publicKey).toBeNull();
+ });
+});
diff --git a/src/store/walletStore.ts b/src/store/walletStore.ts
index b0a54e1..39dd199 100644
--- a/src/store/walletStore.ts
+++ b/src/store/walletStore.ts
@@ -1,21 +1,37 @@
import { create } from "zustand";
+import { getSecretKey } from "@/services/secureStorage";
+import * as StellarSdk from "@stellar/stellar-sdk";
interface WalletState {
isOnboarded: boolean;
publicKey: string | null;
balances: Record;
+ hydrated: boolean;
setOnboarded: (value: boolean) => void;
setPublicKey: (key: string) => void;
setBalances: (balances: Record) => void;
reset: () => void;
+ hydrate: () => Promise;
}
export const useWalletStore = create((set) => ({
isOnboarded: false,
publicKey: null,
balances: {},
+ hydrated: false,
+
setOnboarded: (value) => set({ isOnboarded: value }),
setPublicKey: (key) => set({ publicKey: key }),
setBalances: (balances) => set({ balances }),
reset: () => set({ isOnboarded: false, publicKey: null, balances: {} }),
+
+ hydrate: async () => {
+ const secret = await getSecretKey();
+ if (secret) {
+ const keypair = StellarSdk.Keypair.fromSecret(secret);
+ set({ isOnboarded: true, publicKey: keypair.publicKey(), hydrated: true });
+ } else {
+ set({ hydrated: true });
+ }
+ },
}));