@shaykec/app-agent 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.claude/agents/android-customizer.md +9 -1
  2. package/.claude/agents/ios-customizer.md +9 -1
  3. package/.claude/agents/react-native-customizer.md +71 -0
  4. package/.claude/skills/android-customizer/SKILL.md +95 -23
  5. package/.claude/skills/ios-customizer/SKILL.md +102 -23
  6. package/.claude/skills/react-native-customizer/SKILL.md +85 -11
  7. package/.cursor/agents/android-customizer.md +15 -11
  8. package/.cursor/agents/ios-customizer.md +15 -10
  9. package/.cursor/agents/react-native-customizer.md +170 -0
  10. package/.cursor/mcp.json +2 -10
  11. package/.cursor/skills/android-customizer/SKILL.md +3 -3
  12. package/.cursor/skills/ios-customizer/SKILL.md +3 -3
  13. package/.cursor/skills/react-native-customizer/SKILL.md +69 -9
  14. package/package.json +1 -1
  15. package/templates/android/Skeleton/TESTING_MANIFEST.md +2 -1
  16. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/MainActivity.kt +23 -2
  17. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/theme/AppearanceManager.kt +42 -0
  18. package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/features/profile/ProfileScreen.kt +20 -8
  19. package/templates/android/Skeleton/tests/03_detail_screen.yaml +2 -1
  20. package/templates/android/Skeleton/tests/04_favorites.yaml +2 -1
  21. package/templates/android/Skeleton/tests/08_full_e2e.yaml +2 -1
  22. package/templates/android/Skeleton/tests/09_dark_mode.yaml +50 -0
  23. package/templates/ios/Skeleton/tests/09_dark_mode.yaml +52 -0
  24. package/templates/react-native/Skeleton/TESTING_MANIFEST.md +1 -0
  25. package/templates/react-native/Skeleton/tests/09_dark_mode.yaml +46 -0
@@ -86,18 +86,20 @@ iOS templates use a `Colors.swift` theme file:
86
86
  - Ensure dark mode variants are provided
87
87
  - Use semantic colors where possible (`Color.primary`, `Color.secondary`)
88
88
 
89
- ### MockDataProvider Updates
90
- iOS MockDataProvider uses static methods returning arrays:
89
+ ### Data Layer
90
+ Templates use a `DataRepository` protocol with `DataSourceResolver`:
91
91
  ```swift
92
- struct MockDataProvider {
93
- static func getServices() -> [Service] { ... }
94
- static func getProviders() -> [Provider] { ... }
92
+ // ViewModels depend on the protocol, not MockDataProvider directly
93
+ private let repository: any DataRepository
94
+ init(repository: any DataRepository = DataSourceResolver.repository) {
95
+ self.repository = repository
95
96
  }
97
+ repository.addItem(newItem)
96
98
  ```
97
- - Keep the same function signatures
98
- - Update return values with domain-specific data
99
- - Use realistic Swift date formatting: `Date()`, `Calendar.current`
100
- - Use UUID() for IDs
99
+ - ALWAYS use `DataSourceResolver.repository` in ViewModels, never `MockDataProvider.shared` directly
100
+ - Preserve MockDataProvider CRUD methods and `reset()` function
101
+ - Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
102
+ - Ensure OfflineBanner is integrated into main navigation views
101
103
 
102
104
  ## Output
103
105
 
@@ -116,6 +118,9 @@ POTENTIAL ISSUES:
116
118
  - ONLY edit files under `output/` — never touch `templates/`
117
119
  - Ensure all imports resolve after your changes
118
120
  - Keep `@main` struct and `App` protocol conformance intact
119
- - Preserve `MockDataProvider` function signatures
121
+ - Preserve `MockDataProvider` CRUD methods and `reset()` function
122
+ - Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
120
123
  - Use SF Symbols for tab and navigation icons
124
+ - Preserve ALL `.accessibilityIdentifier()` calls — do NOT remove test IDs
125
+ - Follow `{screen}_{element}_{role}` convention for new test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
121
126
  - Test that view hierarchy compiles (no circular references, no missing types)
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: react-native-customizer
3
+ description: Specialized React Native/TypeScript subagent for customizing React Native app templates. Use when the target platform is React Native and the main agent needs to perform deep customization — updating package configs, rewriting React Native components, adjusting React Navigation, applying themes, and handling cross-platform features.
4
+ tools:
5
+ - Read
6
+ - Edit
7
+ - Write
8
+ - Grep
9
+ - Glob
10
+ - LS
11
+ - Shell
12
+ ---
13
+
14
+ # React Native Customizer
15
+
16
+ You are an expert React Native developer specializing in TypeScript and modern mobile development. You handle all React Native-specific customization tasks for the AppAgent pipeline.
17
+
18
+ ## Context You Receive
19
+
20
+ The main agent passes you:
21
+ - **App name** — display name (e.g., "GigWallet")
22
+ - **App directory** — path under `output/` (e.g., `output/gigwallet-rn/`)
23
+ - **Platform** — `react-native`
24
+ - **App description** — what the app does
25
+ - **What has been done** — renaming, AppConfig, mock data updates already completed
26
+ - **Design brief** (from design-system skill) — color palette (light + dark hex codes), icon mapping, visual tone (playful/professional/minimal/bold), corner radius, and typography intent. Apply these tokens to `ThemeContext.tsx`, tab icons, and layout values.
27
+ - **Content brief** (from content-writer skill) — onboarding text, tab labels, section headers, empty state messages, button labels, search placeholders, and error messages. Apply these strings to React Native components.
28
+
29
+ ## Logging Protocol (MANDATORY)
30
+
31
+ When you start, print:
32
+ `[SUBAGENT:react-native-customizer] Starting — React Native customization for {app-name}`
33
+
34
+ When you finish, print:
35
+ `[SUBAGENT:react-native-customizer] Completed — {count} files modified`
36
+
37
+ ## Your Expertise
38
+
39
+ - React Native 0.73+, TypeScript 5+
40
+ - React Navigation 6+ (bottom tabs, native stack, type-safe navigation)
41
+ - MVVM architecture with custom hooks (`useXxxViewModel`)
42
+ - StyleSheet.create() for performant styling
43
+ - FlatList / SectionList for virtualized lists
44
+ - React Context for theming and global state
45
+ - Cross-platform patterns (iOS + Android from a single codebase)
46
+
47
+ ## Tasks You Handle
48
+
49
+ ### Package & Project Renaming
50
+ When renaming a React Native app:
51
+ 1. Update `package.json` — `name` field
52
+ 2. Update `app.json` — `name` and `displayName`
53
+ 3. Update `android/app/build.gradle` — `applicationId`, `namespace`
54
+ 4. Update `android/settings.gradle` — `rootProject.name`
55
+ 5. Update `android/app/src/main/res/values/strings.xml` — `app_name`
56
+ 6. Update `android/app/src/main/java/` — package directory structure, `MainActivity.kt`, `MainApplication.kt`
57
+ 7. Update `ios/Podfile` — project target name
58
+ 8. Update all `import` statements if module paths changed
59
+
60
+ ### React Native Component Customization
61
+ When modifying components:
62
+ - Use functional components with `React.FC` typing
63
+ - Use custom hooks (`useXxxViewModel`) for state and business logic
64
+ - Use `StyleSheet.create()` for all styles — no inline style objects
65
+ - Use `useTheme()` hook for accessing theme colors and typography
66
+ - Use `FlatList` / `SectionList` for lists (not `map()` on `ScrollView`)
67
+ - Keep components stateless where possible — hoist state to ViewModel hooks
68
+ - Add `testID` props to ALL interactive elements
69
+
70
+ ### Navigation Structure
71
+ Templates use React Navigation with bottom tabs and native stack:
72
+ ```tsx
73
+ const Tab = createBottomTabNavigator();
74
+ const Stack = createNativeStackNavigator<RootStackParamList>();
75
+
76
+ function MainTabs() {
77
+ return (
78
+ <Tab.Navigator>
79
+ <Tab.Screen name="Home" component={HomeScreen} />
80
+ <Tab.Screen name="Explore" component={ExploreScreen} />
81
+ </Tab.Navigator>
82
+ );
83
+ }
84
+
85
+ function AppNavigator() {
86
+ return (
87
+ <NavigationContainer>
88
+ <Stack.Navigator>
89
+ <Stack.Screen name="MainTabs" component={MainTabs} />
90
+ <Stack.Screen name="Detail" component={DetailScreen} />
91
+ </Stack.Navigator>
92
+ </NavigationContainer>
93
+ );
94
+ }
95
+ ```
96
+ When adding/removing/renaming tabs:
97
+ - Update tab names and components in `AppNavigator.tsx`
98
+ - Apply tab labels from the content brief
99
+ - Apply icon text/emoji from the design brief
100
+ - Update `RootStackParamList` type for type-safe navigation
101
+
102
+ ### Theme Customization
103
+ React Native templates use a `ThemeContext.tsx`:
104
+ - Apply primary, secondary, accent colors from the design brief (hex values)
105
+ - Provide both light and dark mode variants
106
+ - Apply corner radius and spacing tokens
107
+ - Apply typography settings (font sizes, weights)
108
+
109
+ ```tsx
110
+ const lightColors = {
111
+ primary: '#007AFF',
112
+ secondary: '#5856D6',
113
+ accent: '#FF9500',
114
+ background: '#FFFFFF',
115
+ surface: '#F2F2F7',
116
+ text: '#000000',
117
+ textSecondary: '#8E8E93',
118
+ border: '#C6C6C8',
119
+ error: '#FF3B30',
120
+ success: '#34C759',
121
+ };
122
+ ```
123
+
124
+ ### Data Layer
125
+ Templates use a `DataRepository` interface with `DataSourceResolver`:
126
+ ```tsx
127
+ import { DataSourceResolver } from '../data/DataSourceResolver';
128
+
129
+ export function useHomeViewModel() {
130
+ const repository = DataSourceResolver.repository;
131
+ const [items, setItems] = useState<Item[]>([]);
132
+ useEffect(() => {
133
+ setItems(repository.getItems());
134
+ const unsubscribe = repository.subscribe(() => setItems(repository.getItems()));
135
+ return unsubscribe;
136
+ }, []);
137
+ return { items };
138
+ }
139
+ ```
140
+ - ALWAYS use `DataSourceResolver.repository` in ViewModel hooks, never `MockDataProvider` directly
141
+ - Preserve MockDataProvider CRUD methods and `reset()` function
142
+ - Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
143
+ - Ensure OfflineBanner is integrated into main navigation screens
144
+
145
+ ## Output
146
+
147
+ When done, report what you changed:
148
+ ```
149
+ FILES MODIFIED:
150
+ - {path}: {what changed}
151
+ - {path}: {what changed}
152
+
153
+ POTENTIAL ISSUES:
154
+ - {any concerns or items needing manual review}
155
+ ```
156
+
157
+ ## Rules
158
+
159
+ - ONLY edit files under `output/` — never touch `templates/`
160
+ - Ensure all imports resolve after your changes
161
+ - Preserve `MockDataProvider` CRUD methods and `reset()` function
162
+ - Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
163
+ - Ensure OfflineBanner is integrated into main navigation screens
164
+ - Use `testID` props on ALL interactive elements
165
+ - Follow `{screen}_{element}_{role}` convention for test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
166
+ - Use `StyleSheet.create()` for styles, not inline objects
167
+ - Use functional components with TypeScript
168
+ - Keep React Navigation structure intact
169
+ - Preserve ThemeContext provider wrapping in App.tsx
170
+ - Do NOT introduce new npm dependencies
package/.cursor/mcp.json CHANGED
@@ -1,11 +1,3 @@
1
1
  {
2
- "mcpServers": {
3
- "ai-tester": {
4
- "command": "node",
5
- "args": ["/Users/shayco/ai-tester/dist/mcp/index.js"],
6
- "env": {
7
- "MCP_LOG_LEVEL": "info"
8
- }
9
- }
10
- }
11
- }
2
+ "mcpServers": {}
3
+ }
@@ -9,7 +9,7 @@ Use this skill to perform deep Android/Kotlin/Compose customization on a cloned
9
9
 
10
10
  ## When to Use
11
11
 
12
- - At Step 8 of the AppAgent workflow, when the platform is Android
12
+ - At Step 5 of the AppAgent workflow, when the platform is Android
13
13
  - After AppConfig, mock data, and design tokens are already in place
14
14
  - When you need to modify Compose screens, navigation, and themes
15
15
 
@@ -282,7 +282,7 @@ Write the report to `output/{app-name}/reports/05-customization.md` (append Andr
282
282
  ```markdown
283
283
  # Customization Report
284
284
 
285
- **Step:** 8
285
+ **Step:** 5
286
286
  **Skill:** android-customizer
287
287
  **Timestamp:** {ISO 8601}
288
288
  **Result:** PASS
@@ -333,7 +333,7 @@ Update `output/{app-name}/reports/summary.json` — read the file, append this s
333
333
 
334
334
  ```json
335
335
  {
336
- "step": 8,
336
+ "step": 5,
337
337
  "name": "android-customization",
338
338
  "startedAt": "{ISO 8601 timestamp}",
339
339
  "durationSeconds": 0,
@@ -9,7 +9,7 @@ Use this skill to perform deep iOS/SwiftUI customization on a cloned template.
9
9
 
10
10
  ## When to Use
11
11
 
12
- - At Step 8 of the AppAgent workflow, when the platform is iOS
12
+ - At Step 5 of the AppAgent workflow, when the platform is iOS
13
13
  - After AppConfig, mock data, and design tokens are already in place
14
14
  - When you need to modify SwiftUI views, navigation, and themes
15
15
 
@@ -262,7 +262,7 @@ Write the report to `output/{app-name}/reports/05-customization.md` (append iOS
262
262
  ```markdown
263
263
  # Customization Report
264
264
 
265
- **Step:** 8
265
+ **Step:** 5
266
266
  **Skill:** ios-customizer
267
267
  **Timestamp:** {ISO 8601}
268
268
  **Result:** PASS
@@ -312,7 +312,7 @@ Update `output/{app-name}/reports/summary.json` — read the file, append this s
312
312
 
313
313
  ```json
314
314
  {
315
- "step": 8,
315
+ "step": 5,
316
316
  "name": "ios-customization",
317
317
  "startedAt": "{ISO 8601 timestamp}",
318
318
  "durationSeconds": 0,
@@ -133,12 +133,15 @@ const lightColors = {
133
133
  };
134
134
  ```
135
135
 
136
- ### Step 5 — Data Layer & Mock Data
136
+ ### Step 5 — Data Layer & Offline Infrastructure
137
137
 
138
- Templates use a `MockDataProvider.ts` that implements `DataRepository` interface:
138
+ Templates include an **offline-first data layer**. Understand these components:
139
139
 
140
140
  **Files you should NOT modify** (generic infrastructure):
141
141
  - `src/data/DataRepository.ts` — interface definition
142
+ - `src/data/NetworkMonitor.ts` — connectivity detection
143
+ - `src/data/LocalPersistence.ts` — JSON file persistence
144
+ - `src/components/OfflineBanner.tsx` — offline status banner
142
145
  - `src/theme/ThemeContext.tsx` — theme provider
143
146
 
144
147
  **Files you MAY need to update** (domain-specific):
@@ -147,25 +150,29 @@ Templates use a `MockDataProvider.ts` that implements `DataRepository` interface
147
150
  - All CRUD methods work with correct entity types
148
151
  - Subscription/onChange pattern is working
149
152
  - Reset method references correct defaults
153
+ - `src/data/SyncManager.ts` — if entity types were renamed, update domain methods
150
154
 
151
155
  **ViewModel patterns** (how screens consume data):
152
156
 
153
- ViewModels are custom React hooks that use MockDataProvider:
157
+ ViewModels are custom React hooks that use the `DataRepository` interface via `DataSourceResolver`, not `MockDataProvider` directly:
154
158
  ```tsx
159
+ import { DataSourceResolver } from '../data/DataSourceResolver';
160
+
155
161
  export function useHomeViewModel() {
162
+ const repository = DataSourceResolver.repository;
156
163
  const [items, setItems] = useState<Item[]>([]);
157
164
  const [isLoading, setIsLoading] = useState(true);
158
165
 
159
166
  useEffect(() => {
160
167
  loadData();
161
- const unsubscribe = MockDataProvider.subscribe(() => {
162
- setItems(MockDataProvider.getItems());
168
+ const unsubscribe = repository.subscribe(() => {
169
+ setItems(repository.getItems());
163
170
  });
164
171
  return unsubscribe;
165
172
  }, []);
166
173
 
167
174
  const loadData = () => {
168
- setItems(MockDataProvider.getItems());
175
+ setItems(repository.getItems());
169
176
  setIsLoading(false);
170
177
  };
171
178
 
@@ -173,6 +180,16 @@ export function useHomeViewModel() {
173
180
  }
174
181
  ```
175
182
 
183
+ When customizing ViewModels, ALWAYS use `DataSourceResolver.repository`, never `MockDataProvider` directly. This ensures the app works with any data source (mock, localStorage, Firebase, Supabase, custom API).
184
+
185
+ **OfflineBanner integration** — add to main navigation screens:
186
+ ```tsx
187
+ <View style={{ flex: 1 }}>
188
+ <OfflineBanner />
189
+ {/* ... rest of screen content */}
190
+ </View>
191
+ ```
192
+
176
193
  ### Step 6 — Data Layer Verification
177
194
 
178
195
  Verify the data layer uses correct patterns:
@@ -193,9 +210,36 @@ export interface DataRepository {
193
210
  }
194
211
  ```
195
212
 
213
+ **MockDataProvider (implements DataRepository):**
214
+ ```tsx
215
+ class MockDataProvider implements DataRepository {
216
+ private static instance: MockDataProvider;
217
+ private listeners: Set<() => void> = new Set();
218
+
219
+ getItems(): Item[] { ... }
220
+ addItem(item: Item): void { ... }
221
+ subscribe(callback: () => void): () => void { ... }
222
+ reset(): void { ... } // Reset to defaults
223
+ }
224
+ ```
225
+
226
+ **DataSourceResolver:**
227
+ ```tsx
228
+ export class DataSourceResolver {
229
+ static get repository(): DataRepository {
230
+ switch (AppConfig.DataSource.active) {
231
+ case 'localStorage': return MockDataProvider.getInstance(); // TODO: LocalStorageProvider
232
+ case 'mock': return MockDataProvider.getInstance();
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
196
238
  Verify:
197
- - MockDataProvider implements all DataRepository methods
198
- - ViewModel hooks use MockDataProvider correctly
239
+ - DataRepository interface matches MockDataProvider's public methods
240
+ - MockDataProvider implements DataRepository
241
+ - DataSourceResolver returns the correct implementation
242
+ - ViewModel hooks use `DataSourceResolver.repository`, not `MockDataProvider` directly
199
243
  - Seed data matches the new domain
200
244
  - CRUD methods reference correct entity types
201
245
 
@@ -237,6 +281,14 @@ import { CelebrationOverlay } from '../shared/CelebrationEffects';
237
281
  4. **Wrap all applied animations** with `useMotionPreferences` checks if MotionPreferences is integrated
238
282
  5. **Do NOT add animation modifiers** to screens not listed in the manifest
239
283
 
284
+ ### Step 6.7 — App Icon (if applicable)
285
+
286
+ React Native apps target both iOS and Android. App icon customization is handled per-platform:
287
+ - **iOS**: If `scripts/generate-app-icon.swift` exists, run it with the SF Symbol and background color from the manifest
288
+ - **Android**: If `templates/android/ICON_VECTORS.md` exists, update `android/app/src/main/res/drawable/ic_launcher_foreground.xml` and `ic_launcher_background` color
289
+
290
+ If the scripts/resources are not available, skip — the template's default icons will be used.
291
+
240
292
  ## Report Output
241
293
 
242
294
  Write the report to `output/{app-name}/reports/05-customization.md`:
@@ -269,6 +321,12 @@ React Native (TypeScript)
269
321
  - Empty states: {count}
270
322
  - CTAs updated: {count}
271
323
 
324
+ ## Offline Infrastructure
325
+
326
+ - OfflineBanner integrated: {yes/no}
327
+ - SyncManager methods updated: {list or "no changes needed"}
328
+ - Data persistence verified: {yes/no}
329
+
272
330
  ## Animation Modifiers Applied
273
331
 
274
332
  - {modifier}: applied to {screen} — {description}
@@ -324,8 +382,10 @@ Templates include `testID` props on all interactive elements and a `TESTING_MANI
324
382
  - ONLY edit files under `output/` — never touch `templates/`
325
383
  - Ensure all imports resolve after your changes
326
384
  - Preserve `MockDataProvider` CRUD methods and `reset()` function
385
+ - Do NOT modify NetworkMonitor, LocalPersistence, or OfflineBanner
386
+ - Ensure OfflineBanner is integrated into main navigation screens
327
387
  - Use `testID` props on ALL interactive elements
328
- - Follow `{screen}_{element}_{role}` convention for new test IDs
388
+ - Follow `{screen}_{element}_{role}` convention for new test IDs (see `templates/TEST_ID_CONVENTIONS.md`)
329
389
  - Use `StyleSheet.create()` for styles, not inline objects
330
390
  - Use functional components with TypeScript
331
391
  - Keep React Navigation structure intact
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaykec/app-agent",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "AI-powered mobile app generator using Cursor CLI and templates",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,7 +48,8 @@ This document catalogs all test tags in the Skeleton template for automated test
48
48
  - `profile_name` — User name
49
49
  - `profile_email` — User email
50
50
  - `profile_notifications` — Notifications setting
51
- - `profile_appearance` — Appearance setting
51
+ - `profile_appearance` — Appearance / Dark Mode row
52
+ - `profile_dark_mode_toggle` — Dark mode toggle switch
52
53
  - `profile_motion_preferences` — Motion Preferences / Animations link
53
54
  - `profile_version` — App version
54
55
  - `profile_privacy` — Privacy policy
@@ -4,18 +4,39 @@ import android.os.Bundle
4
4
  import androidx.activity.ComponentActivity
5
5
  import androidx.activity.compose.setContent
6
6
  import androidx.activity.enableEdgeToEdge
7
+ import androidx.compose.foundation.layout.Box
8
+ import androidx.compose.foundation.layout.fillMaxSize
9
+ import androidx.compose.runtime.CompositionLocalProvider
10
+ import androidx.compose.runtime.remember
11
+ import androidx.compose.ui.ExperimentalComposeUiApi
12
+ import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.semantics.semantics
14
+ import androidx.compose.ui.semantics.testTagsAsResourceId
15
+ import com.appship.skeleton.core.theme.AppearanceManager
16
+ import com.appship.skeleton.core.theme.LocalAppearanceManager
7
17
  import com.appship.skeleton.core.theme.SkeletonTheme
18
+ import com.appship.skeleton.core.theme.resolveIsDarkTheme
8
19
  import com.appship.skeleton.features.navigation.MainScreen
9
20
  import dagger.hilt.android.AndroidEntryPoint
10
21
 
11
22
  @AndroidEntryPoint
12
23
  class MainActivity : ComponentActivity() {
24
+ @OptIn(ExperimentalComposeUiApi::class)
13
25
  override fun onCreate(savedInstanceState: Bundle?) {
14
26
  super.onCreate(savedInstanceState)
15
27
  enableEdgeToEdge()
16
28
  setContent {
17
- SkeletonTheme {
18
- MainScreen()
29
+ val appearanceManager = remember { AppearanceManager(this) }
30
+ CompositionLocalProvider(LocalAppearanceManager provides appearanceManager) {
31
+ SkeletonTheme(darkTheme = resolveIsDarkTheme(appearanceManager)) {
32
+ Box(
33
+ modifier = Modifier
34
+ .fillMaxSize()
35
+ .semantics { testTagsAsResourceId = true }
36
+ ) {
37
+ MainScreen()
38
+ }
39
+ }
19
40
  }
20
41
  }
21
42
  }
@@ -0,0 +1,42 @@
1
+ package com.appship.skeleton.core.theme
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import androidx.compose.foundation.isSystemInDarkTheme
6
+ import androidx.compose.runtime.*
7
+ import androidx.compose.runtime.staticCompositionLocalOf
8
+
9
+ // CUSTOMIZE:APPEARANCE - Dark mode and appearance management
10
+ enum class AppearanceMode { SYSTEM, LIGHT, DARK }
11
+
12
+ class AppearanceManager(context: Context) {
13
+ private val prefs: SharedPreferences =
14
+ context.getSharedPreferences("appearance_prefs", Context.MODE_PRIVATE)
15
+
16
+ var appearanceMode: AppearanceMode by mutableStateOf(
17
+ try {
18
+ AppearanceMode.valueOf(prefs.getString("appearance_mode", "SYSTEM") ?: "SYSTEM")
19
+ } catch (_: Exception) {
20
+ AppearanceMode.SYSTEM
21
+ }
22
+ )
23
+ private set
24
+
25
+ fun setMode(mode: AppearanceMode) {
26
+ appearanceMode = mode
27
+ prefs.edit().putString("appearance_mode", mode.name).apply()
28
+ }
29
+ }
30
+
31
+ val LocalAppearanceManager = staticCompositionLocalOf<AppearanceManager> {
32
+ error("No AppearanceManager provided")
33
+ }
34
+
35
+ @Composable
36
+ fun resolveIsDarkTheme(appearanceManager: AppearanceManager): Boolean {
37
+ return when (appearanceManager.appearanceMode) {
38
+ AppearanceMode.SYSTEM -> isSystemInDarkTheme()
39
+ AppearanceMode.LIGHT -> false
40
+ AppearanceMode.DARK -> true
41
+ }
42
+ }
@@ -17,6 +17,8 @@ import androidx.compose.ui.res.stringResource
17
17
  import androidx.compose.ui.unit.dp
18
18
  import com.appship.skeleton.AppConfig
19
19
  import com.appship.skeleton.R
20
+ import com.appship.skeleton.core.theme.AppearanceMode
21
+ import com.appship.skeleton.core.theme.LocalAppearanceManager
20
22
 
21
23
  // PLACEHOLDER: Customize this screen for user profile and settings.
22
24
 
@@ -94,14 +96,24 @@ fun ProfileScreen(
94
96
  .testTag("profile_notifications")
95
97
  )
96
98
  HorizontalDivider()
97
- ListItem(
98
- headlineContent = { Text("Appearance") },
99
- leadingContent = { Icon(Icons.Default.Palette, contentDescription = null) },
100
- trailingContent = { Icon(Icons.Default.ChevronRight, contentDescription = null) },
101
- modifier = Modifier
102
- .clickable { /* Navigate to appearance */ }
103
- .testTag("profile_appearance")
104
- )
99
+ run {
100
+ val appearanceManager = LocalAppearanceManager.current
101
+ val isDark = appearanceManager.appearanceMode == AppearanceMode.DARK
102
+ ListItem(
103
+ headlineContent = { Text("Dark Mode") },
104
+ leadingContent = { Icon(Icons.Default.DarkMode, contentDescription = null) },
105
+ trailingContent = {
106
+ Switch(
107
+ checked = isDark,
108
+ onCheckedChange = { checked ->
109
+ appearanceManager.setMode(if (checked) AppearanceMode.DARK else AppearanceMode.SYSTEM)
110
+ },
111
+ modifier = Modifier.testTag("profile_dark_mode_toggle")
112
+ )
113
+ },
114
+ modifier = Modifier.testTag("profile_appearance")
115
+ )
116
+ }
105
117
  if (AppConfig.Features.ENABLE_ANIMATIONS) {
106
118
  HorizontalDivider()
107
119
  ListItem(
@@ -7,7 +7,8 @@ appId: ${APP_PACKAGE_NAME}
7
7
  timeout: 3000
8
8
  - assertVisible: "Home"
9
9
  - extendedWaitUntil:
10
- visible: "home_item_0_card"
10
+ visible:
11
+ id: "home_item_0_card"
11
12
  timeout: 2000
12
13
  - tapOn:
13
14
  id: "home_item_0_card"
@@ -7,7 +7,8 @@ appId: ${APP_PACKAGE_NAME}
7
7
  timeout: 3000
8
8
  - assertVisible: "Home"
9
9
  - extendedWaitUntil:
10
- visible: "home_item_0_card"
10
+ visible:
11
+ id: "home_item_0_card"
11
12
  timeout: 2000
12
13
  # Favorite an item first
13
14
  - tapOn:
@@ -8,7 +8,8 @@ appId: ${APP_PACKAGE_NAME}
8
8
  # Home tab
9
9
  - assertVisible: "Home"
10
10
  - extendedWaitUntil:
11
- visible: "home_item_0_card"
11
+ visible:
12
+ id: "home_item_0_card"
12
13
  timeout: 2000
13
14
  # Detail from home
14
15
  - tapOn:
@@ -0,0 +1,50 @@
1
+ appId: ${APP_PACKAGE_NAME}
2
+ ---
3
+ - clearState
4
+ - launchApp
5
+ - extendedWaitUntil:
6
+ visible: "Home"
7
+ timeout: 3000
8
+ # Navigate to Profile
9
+ - tapOn: "Profile"
10
+ - extendedWaitUntil:
11
+ visible:
12
+ id: "profile_avatar"
13
+ timeout: 3000
14
+ # Verify dark mode toggle is visible
15
+ - assertVisible:
16
+ id: "profile_dark_mode_toggle"
17
+ # Toggle dark mode ON
18
+ - tapOn:
19
+ id: "profile_dark_mode_toggle"
20
+ # Verify profile still renders in dark mode
21
+ - assertVisible:
22
+ id: "profile_avatar"
23
+ optional: true
24
+ - assertVisible:
25
+ id: "profile_name"
26
+ optional: true
27
+ # Navigate to Home to verify dark mode persists across tabs
28
+ - tapOn: "Home"
29
+ - extendedWaitUntil:
30
+ visible: "Home"
31
+ timeout: 3000
32
+ # Navigate back to Profile
33
+ - tapOn: "Profile"
34
+ - extendedWaitUntil:
35
+ visible:
36
+ id: "profile_avatar"
37
+ timeout: 3000
38
+ # Verify toggle is still ON (dark mode persisted)
39
+ - assertVisible:
40
+ id: "profile_dark_mode_toggle"
41
+ # Toggle dark mode OFF (back to system)
42
+ - tapOn:
43
+ id: "profile_dark_mode_toggle"
44
+ # Verify profile renders normally
45
+ - assertVisible:
46
+ id: "profile_avatar"
47
+ optional: true
48
+ - assertVisible:
49
+ id: "profile_name"
50
+ optional: true