Skip to content

Commit 1b9bd7f

Browse files
author
bealqiu
committed
feat(expo): 添加 Expo 移动端应用支持
1 parent d6464d1 commit 1b9bd7f

30 files changed

Lines changed: 9506 additions & 3113 deletions

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,39 @@ jobs:
6262
- name: Install frontend dependencies
6363
run: pnpm install
6464

65+
- name: Import Apple certificate (macOS only)
66+
if: runner.os == 'macOS'
67+
env:
68+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
69+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
70+
run: |
71+
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
72+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
73+
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
74+
75+
# 解码证书
76+
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
77+
78+
# 创建临时钥匙串
79+
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
80+
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
81+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
82+
83+
# 导入证书到临时钥匙串
84+
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
85+
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
86+
security list-keychain -d user -s $KEYCHAIN_PATH
87+
6588
- name: Build Tauri app
6689
uses: tauri-apps/tauri-action@v0
6790
env:
6891
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
92+
# macOS 代码签名
93+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
94+
# macOS 公证(Notarization)
95+
APPLE_ID: ${{ secrets.APPLE_ID }}
96+
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
97+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
6998
with:
7099
projectPath: "./packages/app"
71100
tagName: ${{ github.ref_name }}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"dev": "pnpm --filter app dev",
66
"build": "pnpm --filter app build",
77
"tauri": "pnpm --filter app tauri",
8+
"expo:start": "pnpm --filter @readany/app-expo start",
9+
"expo:ios": "pnpm --filter @readany/app-expo ios",
10+
"expo:android": "pnpm --filter @readany/app-expo android",
11+
"expo:prebuild": "pnpm --filter @readany/app-expo prebuild",
812
"lint": "biome check .",
913
"lint:fix": "biome check --write ."
1014
},

packages/app-expo/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
node_modules/
2+
dist/
3+
.expo/
4+
ios/
5+
android/
6+
*.jks
7+
*.p8
8+
*.p12
9+
*.key
10+
*.mobileprovision
11+
*.orig.*
12+
web-build/

packages/app-expo/app.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"expo": {
3+
"name": "ReadAny",
4+
"slug": "readany",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"icon": "./assets/icon.png",
8+
"userInterfaceStyle": "automatic",
9+
"newArchEnabled": true,
10+
"splash": {
11+
"image": "./assets/splash-icon.png",
12+
"resizeMode": "contain",
13+
"backgroundColor": "#0a0a0a"
14+
},
15+
"ios": {
16+
"supportsTablet": true,
17+
"bundleIdentifier": "com.readany.app"
18+
},
19+
"android": {
20+
"adaptiveIcon": {
21+
"foregroundImage": "./assets/adaptive-icon.png",
22+
"backgroundColor": "#0a0a0a"
23+
},
24+
"package": "com.readany.app"
25+
},
26+
"plugins": [
27+
"expo-font",
28+
"expo-secure-store",
29+
"expo-sqlite"
30+
],
31+
"scheme": "readany"
32+
}
33+
}

packages/app-expo/assets/.gitkeep

Whitespace-only changes.

packages/app-expo/babel.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = function (api) {
2+
api.cache(true);
3+
return {
4+
presets: ["babel-preset-expo"],
5+
plugins: [
6+
[
7+
"module-resolver",
8+
{
9+
alias: {
10+
"@": "./src",
11+
},
12+
},
13+
],
14+
"react-native-reanimated/plugin",
15+
],
16+
};
17+
};

packages/app-expo/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { registerRootComponent } from "expo";
2+
import App from "./src/App";
3+
4+
registerRootComponent(App);

packages/app-expo/metro.config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const { getDefaultConfig } = require("expo/metro-config");
2+
const path = require("path");
3+
4+
const projectRoot = __dirname;
5+
const monorepoRoot = path.resolve(projectRoot, "../..");
6+
7+
const config = getDefaultConfig(projectRoot);
8+
9+
// 1. Watch the monorepo root so Metro can resolve workspace packages
10+
config.watchFolders = [monorepoRoot];
11+
12+
// 2. Tell Metro where to find node_modules in a pnpm monorepo
13+
config.resolver.nodeModulesPaths = [
14+
path.resolve(projectRoot, "node_modules"),
15+
path.resolve(monorepoRoot, "node_modules"),
16+
];
17+
18+
// 3. Ensure @readany/core source files are resolved
19+
config.resolver.disableHierarchicalLookup = false;
20+
21+
module.exports = config;

packages/app-expo/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@readany/app-expo",
3+
"version": "1.0.0",
4+
"private": true,
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "expo start",
8+
"android": "expo run:android",
9+
"ios": "expo run:ios",
10+
"prebuild": "expo prebuild",
11+
"lint": "biome check ."
12+
},
13+
"dependencies": {
14+
"@readany/core": "workspace:*",
15+
16+
"expo": "~55.0.5",
17+
"expo-asset": "~55.0.8",
18+
"expo-clipboard": "~55.0.8",
19+
"expo-constants": "~55.0.7",
20+
"expo-file-system": "~55.0.10",
21+
"expo-font": "~55.0.4",
22+
"expo-linking": "~55.0.7",
23+
"expo-secure-store": "~55.0.8",
24+
"expo-sharing": "~55.0.11",
25+
"expo-speech": "~55.0.8",
26+
"expo-splash-screen": "~55.0.10",
27+
"expo-sqlite": "~55.0.10",
28+
"expo-status-bar": "~55.0.4",
29+
30+
"react": "^19.0.0",
31+
"react-native": "~0.84.1",
32+
"react-native-gesture-handler": "~2.30.0",
33+
"react-native-reanimated": "~4.2.2",
34+
"react-native-safe-area-context": "~5.7.0",
35+
"react-native-screens": "~4.24.0",
36+
"react-native-svg": "~15.15.3",
37+
"react-native-webview": "~13.16.1",
38+
39+
"@react-navigation/native": "^7.1.33",
40+
"@react-navigation/bottom-tabs": "^7.15.5",
41+
"@react-navigation/native-stack": "^7.14.4",
42+
43+
"i18next": "^25.8.13",
44+
"react-i18next": "^16.5.4",
45+
"zustand": "^5.0.5"
46+
},
47+
"devDependencies": {
48+
"@babel/core": "^7.25.0",
49+
"@types/react": "^19.0.0",
50+
"babel-plugin-module-resolver": "^5.0.2",
51+
"typescript": "^5.8.0"
52+
}
53+
}

packages/app-expo/src/App.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* ReadAny Expo App — Root component
3+
*
4+
* Initialises platform service, i18n, and mounts navigation.
5+
*/
6+
import { useEffect, useState } from "react";
7+
import { ActivityIndicator, View } from "react-native";
8+
import { SafeAreaProvider } from "react-native-safe-area-context";
9+
import { GestureHandlerRootView } from "react-native-gesture-handler";
10+
import { NavigationContainer } from "@react-navigation/native";
11+
import { StatusBar } from "expo-status-bar";
12+
13+
import { setPlatformService } from "@readany/core/services";
14+
import { initI18nLanguage } from "@readany/core/i18n";
15+
import { setSessionEventSource } from "@readany/core/hooks";
16+
import { setTTSPlayerFactories } from "@readany/core/stores";
17+
18+
import { ExpoPlatformService } from "@/lib/platform/expo-platform-service";
19+
import { rnSessionEventSource } from "@/lib/platform/rn-session-events";
20+
import { rnTTSPlayerFactories } from "@/lib/platform/rn-tts-factories";
21+
import { RootNavigator } from "@/navigation/RootNavigator";
22+
23+
export default function App() {
24+
const [ready, setReady] = useState(false);
25+
26+
useEffect(() => {
27+
async function bootstrap() {
28+
// 1. Register platform service
29+
const platform = new ExpoPlatformService();
30+
setPlatformService(platform);
31+
32+
// 2. Register RN-specific adapters
33+
setSessionEventSource(rnSessionEventSource);
34+
setTTSPlayerFactories(rnTTSPlayerFactories);
35+
36+
// 3. Restore persisted language
37+
await initI18nLanguage();
38+
39+
setReady(true);
40+
}
41+
bootstrap();
42+
}, []);
43+
44+
if (!ready) {
45+
return (
46+
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#0a0a0a" }}>
47+
<ActivityIndicator size="large" color="#6366f1" />
48+
</View>
49+
);
50+
}
51+
52+
return (
53+
<GestureHandlerRootView style={{ flex: 1 }}>
54+
<SafeAreaProvider>
55+
<NavigationContainer>
56+
<StatusBar style="auto" />
57+
<RootNavigator />
58+
</NavigationContainer>
59+
</SafeAreaProvider>
60+
</GestureHandlerRootView>
61+
);
62+
}

0 commit comments

Comments
 (0)