feat: production-ready UI with onboarding wizard and dashboard
- Onboarding flow (welcome -> Codex setup -> Claude setup -> done) with dark-themed screens and step indicators; completion stored in SecureStore - Smart root redirect checks onboarding state on launch - Dashboard tab shows both services at a glance via ServiceStatusCard; useFocusEffect + reload() keeps it fresh when credentials change in tabs - Settings tab manages credentials and exposes re-run setup wizard - Polished Codex and Claude detail screens with service headers and empty states - 4-tab layout (Home, Codex, Claude, Settings) with dark-mode tab bar - Both hooks gain reload() for cross-tab credential refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function OnboardingLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "slide_from_right",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { saveClaudeSessionKey, loadClaudeSessionKey } from "@/lib/storage";
|
||||
|
||||
export default function OnboardingClaudeScreen() {
|
||||
const [savedKey, setSavedKey] = useState<string | null>(null);
|
||||
const [pendingKey, setPendingKey] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadClaudeSessionKey().then((k) => {
|
||||
if (k) setSavedKey(k);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const canSave = pendingKey.trim().startsWith("sk-ant-");
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const trimmed = pendingKey.trim();
|
||||
if (!trimmed.startsWith("sk-ant-")) {
|
||||
setError("Session key must start with sk-ant-");
|
||||
return;
|
||||
}
|
||||
await saveClaudeSessionKey(trimmed);
|
||||
setSavedKey(trimmed);
|
||||
setPendingKey("");
|
||||
setError(null);
|
||||
}, [pendingKey]);
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<KeyboardAvoidingView
|
||||
className="flex-1"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View className="flex-1 px-8 justify-between py-8">
|
||||
{/* Top: back + step indicator */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="w-9 h-9 rounded-xl bg-neutral-900 items-center justify-center"
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<View
|
||||
className="w-6 h-6 rounded-full items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
>
|
||||
<MaterialIcons name="check" size={14} color="white" />
|
||||
</View>
|
||||
<View className="w-12 h-0.5" style={{ backgroundColor: "#d97706" }} />
|
||||
<View
|
||||
className="w-6 h-6 rounded-full items-center justify-center"
|
||||
style={{ backgroundColor: "#d97706" }}
|
||||
>
|
||||
<Text className="text-white text-xs font-bold">2</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="w-9" />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={32} color="#d97706" />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-white mb-3">
|
||||
Connect Claude.ai
|
||||
</Text>
|
||||
<Text className="text-neutral-400 text-base leading-relaxed mb-8">
|
||||
Enter your session key to monitor your Claude.ai usage windows.
|
||||
</Text>
|
||||
|
||||
{savedKey ? (
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800 mb-4">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={22} color="#d97706" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold">Connected</Text>
|
||||
<Text className="text-neutral-500 text-sm font-mono">
|
||||
{savedKey.slice(0, 16)}…
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setSavedKey(null);
|
||||
setPendingKey("");
|
||||
}}
|
||||
>
|
||||
<Text className="text-neutral-400 text-xs">Change</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
{error && (
|
||||
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
||||
<Text className="text-red-400 text-sm">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
<TextInput
|
||||
value={pendingKey}
|
||||
onChangeText={(t) => {
|
||||
setPendingKey(t);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="sk-ant-..."
|
||||
placeholderTextColor="#525252"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
className="border border-neutral-800 rounded-xl px-4 py-3.5 text-sm text-white bg-neutral-900 mb-3"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={handleSave}
|
||||
disabled={!canSave}
|
||||
className="rounded-2xl py-4 items-center mb-4"
|
||||
style={{ backgroundColor: canSave ? "#d97706" : "#1a1a1a" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
style={{ color: canSave ? "white" : "#525252" }}
|
||||
>
|
||||
Save Session Key
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View className="bg-neutral-900 rounded-xl p-4 border border-neutral-800">
|
||||
<Text className="text-neutral-400 text-xs font-semibold uppercase tracking-widest mb-2">
|
||||
How to find your session key
|
||||
</Text>
|
||||
<Text className="text-neutral-500 text-sm leading-relaxed">
|
||||
Open Claude.ai in Chrome → DevTools (F12) → Application → Cookies → claude.ai → find{" "}
|
||||
<Text className="text-neutral-300 font-mono">sessionKey</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Bottom actions */}
|
||||
<View className="gap-y-3">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/done")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: savedKey ? "#d97706" : "#1a1a1a" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
style={{ color: savedKey ? "white" : "#a3a3a3" }}
|
||||
>
|
||||
Continue
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/done")}
|
||||
className="py-3 items-center"
|
||||
>
|
||||
<Text className="text-neutral-600 text-sm">Skip for now</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
||||
import { saveCodexAuth, loadCodexAuth } from "@/lib/storage";
|
||||
import type { CodexAuth } from "@/types/codex";
|
||||
|
||||
export default function OnboardingCodexScreen() {
|
||||
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadCodexAuth().then((stored) => {
|
||||
if (stored) setAuth(stored);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const importFile = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const parsed = await pickAndReadCodexAuth();
|
||||
await saveCodexAuth(parsed);
|
||||
setAuth(parsed);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
||||
if (msg !== "PICKER_CANCELLED") setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<View className="flex-1 px-8 justify-between py-8">
|
||||
{/* Top: back + step indicator */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="w-9 h-9 rounded-xl bg-neutral-900 items-center justify-center"
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={18} color="#a3a3a3" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<View className="w-6 h-6 rounded-full items-center justify-center" style={{ backgroundColor: "#10a37f" }}>
|
||||
<Text className="text-white text-xs font-bold">1</Text>
|
||||
</View>
|
||||
<View className="w-12 h-0.5 bg-neutral-800" />
|
||||
<View className="w-6 h-6 rounded-full bg-neutral-800 items-center justify-center">
|
||||
<Text className="text-neutral-500 text-xs font-bold">2</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="w-9" />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center mb-6"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={32} color="#10a37f" />
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-white mb-3">
|
||||
Connect Codex CLI
|
||||
</Text>
|
||||
<Text className="text-neutral-400 text-base leading-relaxed mb-8">
|
||||
Import your{" "}
|
||||
<Text className="text-neutral-200 font-mono text-sm">
|
||||
~/.codex/auth.json
|
||||
</Text>{" "}
|
||||
file to track your rate limits and credits.
|
||||
</Text>
|
||||
|
||||
{/* Status / action */}
|
||||
{auth ? (
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800 mb-4">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons name="check-circle" size={22} color="#10a37f" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold">Connected</Text>
|
||||
{auth.accountId && (
|
||||
<Text className="text-neutral-500 text-sm font-mono">
|
||||
{auth.accountId.slice(0, 12)}…
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity onPress={importFile}>
|
||||
<Text className="text-neutral-400 text-xs">Re-import</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
{error && (
|
||||
<View className="bg-red-950 border border-red-900 rounded-xl px-4 py-3 mb-4">
|
||||
<Text className="text-red-400 text-sm">{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={importFile}
|
||||
disabled={loading}
|
||||
className="rounded-2xl py-4 items-center mb-3"
|
||||
style={{ backgroundColor: loading ? "#1a1a1a" : "#10a37f" }}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#10a37f" />
|
||||
) : (
|
||||
<View className="flex-row items-center gap-x-2">
|
||||
<MaterialIcons name="upload-file" size={18} color="white" />
|
||||
<Text className="text-white font-bold text-base">
|
||||
Import auth.json
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<Text className="text-neutral-600 text-xs text-center">
|
||||
Located at ~/.codex/auth.json on your Mac
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Bottom actions */}
|
||||
<View className="gap-y-3">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/claude")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: auth ? "#10a37f" : "#1a1a1a" }}
|
||||
>
|
||||
<Text
|
||||
className="font-bold text-base"
|
||||
style={{ color: auth ? "white" : "#a3a3a3" }}
|
||||
>
|
||||
Continue
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/claude")}
|
||||
className="py-3 items-center"
|
||||
>
|
||||
<Text className="text-neutral-600 text-sm">Skip for now</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { markOnboardingComplete } from "@/lib/setupState";
|
||||
import { loadCodexAuth, loadClaudeSessionKey } from "@/lib/storage";
|
||||
|
||||
export default function OnboardingDoneScreen() {
|
||||
const [codexConfigured, setCodexConfigured] = useState(false);
|
||||
const [claudeConfigured, setClaudeConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadCodexAuth(), loadClaudeSessionKey()]).then(
|
||||
([codex, claude]) => {
|
||||
setCodexConfigured(!!codex);
|
||||
setClaudeConfigured(!!claude);
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleEnterApp = async () => {
|
||||
await markOnboardingComplete();
|
||||
router.replace("/(tabs)");
|
||||
};
|
||||
|
||||
const noneConfigured = !codexConfigured && !claudeConfigured;
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<View className="flex-1 px-8 items-center justify-center">
|
||||
{/* Icon */}
|
||||
<View
|
||||
className="w-20 h-20 rounded-3xl items-center justify-center mb-8"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={noneConfigured ? "info-outline" : "check-circle"}
|
||||
size={40}
|
||||
color={noneConfigured ? "#a3a3a3" : "#10a37f"}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<Text className="text-4xl font-bold text-white mb-3 text-center">
|
||||
{noneConfigured ? "Almost there" : "You're all set!"}
|
||||
</Text>
|
||||
<Text className="text-neutral-400 text-base text-center leading-relaxed mb-10">
|
||||
{noneConfigured
|
||||
? "No services configured yet. You can set them up anytime in Settings."
|
||||
: "Your services are connected and ready to monitor."}
|
||||
</Text>
|
||||
|
||||
{/* Summary cards */}
|
||||
<View className="w-full gap-y-3 mb-10">
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: codexConfigured ? "#10a37f22" : "#27272a",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={codexConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||
size={22}
|
||||
color={codexConfigured ? "#10a37f" : "#525252"}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className="font-semibold"
|
||||
style={{ color: codexConfigured ? "white" : "#525252" }}
|
||||
>
|
||||
Codex CLI
|
||||
</Text>
|
||||
<Text className="text-neutral-600 text-sm">
|
||||
{codexConfigured ? "Connected" : "Not configured"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: claudeConfigured ? "#d9770622" : "#27272a",
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={claudeConfigured ? "check-circle" : "radio-button-unchecked"}
|
||||
size={22}
|
||||
color={claudeConfigured ? "#d97706" : "#525252"}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className="font-semibold"
|
||||
style={{ color: claudeConfigured ? "white" : "#525252" }}
|
||||
>
|
||||
Claude.ai
|
||||
</Text>
|
||||
<Text className="text-neutral-600 text-sm">
|
||||
{claudeConfigured ? "Connected" : "Not configured"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* CTA */}
|
||||
<TouchableOpacity
|
||||
onPress={handleEnterApp}
|
||||
className="w-full rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
>
|
||||
<Text className="text-white font-bold text-base">Open App</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { router } from "expo-router";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
|
||||
export default function WelcomeScreen() {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-neutral-950">
|
||||
<View className="flex-1 px-8 justify-between py-12">
|
||||
{/* Logo + title */}
|
||||
<View className="items-center mt-8">
|
||||
<View className="flex-row items-center justify-center mb-8">
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={28} color="white" />
|
||||
</View>
|
||||
<View
|
||||
className="w-16 h-16 rounded-2xl items-center justify-center -ml-5"
|
||||
style={{ backgroundColor: "#d97706" }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={28} color="white" />
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-5xl font-bold text-white mb-4 tracking-tight">
|
||||
Codexbar
|
||||
</Text>
|
||||
<Text className="text-neutral-400 text-base text-center leading-relaxed">
|
||||
Monitor your Codex CLI and Claude.ai{"\n"}usage limits at a glance.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Feature pills */}
|
||||
<View className="gap-y-3">
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#10a37f22" }}
|
||||
>
|
||||
<MaterialIcons name="auto-awesome" size={20} color="#10a37f" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold mb-0.5">Codex CLI</Text>
|
||||
<Text className="text-neutral-500 text-sm">
|
||||
Rate limits · Credits · Windows
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-x-3 bg-neutral-900 rounded-2xl p-4 border border-neutral-800">
|
||||
<View
|
||||
className="w-10 h-10 rounded-xl items-center justify-center"
|
||||
style={{ backgroundColor: "#d9770622" }}
|
||||
>
|
||||
<MaterialIcons name="psychology" size={20} color="#d97706" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white font-semibold mb-0.5">Claude.ai</Text>
|
||||
<Text className="text-neutral-500 text-sm">
|
||||
5-hour · 7-day · Model usage
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* CTA */}
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/onboarding/codex")}
|
||||
className="rounded-2xl py-4 items-center"
|
||||
style={{ backgroundColor: "#10a37f" }}
|
||||
>
|
||||
<Text className="text-white font-bold text-base">Get Started</Text>
|
||||
</TouchableOpacity>
|
||||
<Text className="text-neutral-600 text-xs text-center mt-4">
|
||||
You can configure services at any time in Settings
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user