Files
Kokorone/apps/mobile/app/(tabs)/settings.tsx
T
JefferyandClaude Sonnet 5 5da8b3a63e feat: 完成 Q 群組 — APP(React Native + Expo)
新增 apps/mobile:Expo Router 檔案系統路由(同 Next.js App Router 的
心智模型),底部四分頁(聊天/角色/日常/設定)皆為真實 API 串接(/chat、
/tachie、/characters、/schedule、/health、/push)。聊天分頁完整重用
H/O 群組已驗證過的對話+表情查詢節奏。立繪暫以依情緒 magnitude 調整
透明度的色塊佔位(PixiJS 是瀏覽器 DOM/Canvas API,未移植到 React
Native,需要 react-native-skia 等原生繪圖套件才能重現分層合成,超出
本群組範圍)。

新增 apps/api/src/push/:PushProvider 抽象(第三次套用 F-1/P-4 的
Provider 模式),MockPushProvider 供測試、ExpoPushProvider 是可直接
打真實 HTTP 請求的完整實作(Expo 推播匿名發送不需金鑰,不像 LLM/TTS
需要另外挑供應商待人工確認)。PushToken/PushNotificationLog 記錄裝置
註冊與每次推播嘗試。J 群組 TaskTriggerService 觸發時會嘗試推播給
使用者已註冊裝置,用觸發訊息本身當推播內容。

本環境沒有 Android/iOS 模擬器,改用 `expo start --web` + playwright-core
(同 O 群組手法)驅動無頭瀏覽器驗證四分頁切換、真實 API 串接、聊天
完整往返、推播按鈕在無裝置環境下的優雅降級——不是原生 APP 的完整驗證,
但驗證了畫面邏輯與 API 契約層。過程中發現並修正 CORS 設定只放行網頁版
origin、擋掉 Expo 網頁版請求的問題(永久修正:新增 localhost:8081 白名單,
不影響原生 APP,原生環境沒有 CORS 限制)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 19:19:20 +08:00

82 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
import { fetchHealth, registerPushToken } from "../../lib/api";
const USER_ID = "seed-user-primary";
// Q-1 驗收:APP 能呼叫 api 的 /health 並顯示結果。
// Q-3 推播通知:請求權限、取得 Expo push token、註冊到後端——實機/模擬器才能真的收到推播,
// 這裡的按鈕操作與畫面回饋在沒有實體裝置的環境下也能驗證到「流程本身有沒有跑起來、有沒有回報錯誤」。
export default function SettingsScreen() {
const [healthStatus, setHealthStatus] = useState<string | null>(null);
const [healthError, setHealthError] = useState<string | null>(null);
const [pushStatus, setPushStatus] = useState<string | null>(null);
async function checkHealth() {
setHealthError(null);
try {
const result = await fetchHealth();
setHealthStatus(result.status);
} catch {
setHealthError("無法連線到 api 服務");
}
}
async function enablePushNotifications() {
setPushStatus("請求權限中…");
if (!Device.isDevice) {
setPushStatus("推播僅能在實機或支援的模擬器上取得 token(目前環境無法取得真實 token)");
return;
}
const { status } = await Notifications.requestPermissionsAsync();
if (status !== "granted") {
setPushStatus("使用者未授權推播通知");
return;
}
try {
const tokenResponse = await Notifications.getExpoPushTokenAsync();
await registerPushToken(USER_ID, tokenResponse.data, "expo");
setPushStatus(`已註冊推播 token:${tokenResponse.data}`);
} catch (err) {
setPushStatus(`推播 token 取得失敗:${err instanceof Error ? err.message : String(err)}`);
}
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>設定</Text>
<View style={styles.card}>
<Text style={styles.cardTitle}>API 連線狀態</Text>
<Pressable style={styles.button} onPress={checkHealth}>
<Text style={styles.buttonText}>檢查連線</Text>
</Pressable>
{healthStatus && <Text style={styles.line}>狀態:{healthStatus}</Text>}
{healthError && <Text style={styles.error}>{healthError}</Text>}
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>推播通知</Text>
<Pressable style={styles.button} onPress={enablePushNotifications}>
<Text style={styles.buttonText}>開啟推播通知</Text>
</Pressable>
{pushStatus && <Text style={styles.line}>{pushStatus}</Text>}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#FFF5F7" },
content: { padding: 20, gap: 24 },
title: { fontSize: 22, fontWeight: "bold", color: "#FF7E9D" },
card: { gap: 10 },
cardTitle: { fontSize: 15, fontWeight: "bold", color: "#222" },
button: { alignSelf: "flex-start", backgroundColor: "#FF7E9D", borderRadius: 999, paddingHorizontal: 16, paddingVertical: 8 },
buttonText: { color: "#fff", fontWeight: "bold" },
line: { fontSize: 13, color: "#222" },
error: { color: "#FF7E9D", fontSize: 12 },
});