@zigrivers/scaffold 3.6.0 → 3.8.0

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 (115) hide show
  1. package/README.md +127 -12
  2. package/content/knowledge/backend/backend-api-design.md +103 -0
  3. package/content/knowledge/backend/backend-architecture.md +100 -0
  4. package/content/knowledge/backend/backend-async-patterns.md +101 -0
  5. package/content/knowledge/backend/backend-auth-patterns.md +100 -0
  6. package/content/knowledge/backend/backend-conventions.md +105 -0
  7. package/content/knowledge/backend/backend-data-modeling.md +102 -0
  8. package/content/knowledge/backend/backend-deployment.md +100 -0
  9. package/content/knowledge/backend/backend-dev-environment.md +102 -0
  10. package/content/knowledge/backend/backend-observability.md +102 -0
  11. package/content/knowledge/backend/backend-project-structure.md +100 -0
  12. package/content/knowledge/backend/backend-requirements.md +103 -0
  13. package/content/knowledge/backend/backend-security.md +104 -0
  14. package/content/knowledge/backend/backend-testing.md +101 -0
  15. package/content/knowledge/backend/backend-worker-patterns.md +100 -0
  16. package/content/knowledge/cli/cli-architecture.md +101 -0
  17. package/content/knowledge/cli/cli-conventions.md +117 -0
  18. package/content/knowledge/cli/cli-dev-environment.md +121 -0
  19. package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
  20. package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
  21. package/content/knowledge/cli/cli-output-patterns.md +107 -0
  22. package/content/knowledge/cli/cli-project-structure.md +124 -0
  23. package/content/knowledge/cli/cli-requirements.md +101 -0
  24. package/content/knowledge/cli/cli-shell-integration.md +130 -0
  25. package/content/knowledge/cli/cli-testing.md +134 -0
  26. package/content/knowledge/library/library-api-design.md +306 -0
  27. package/content/knowledge/library/library-architecture.md +247 -0
  28. package/content/knowledge/library/library-bundling.md +244 -0
  29. package/content/knowledge/library/library-conventions.md +229 -0
  30. package/content/knowledge/library/library-dev-environment.md +220 -0
  31. package/content/knowledge/library/library-documentation.md +300 -0
  32. package/content/knowledge/library/library-project-structure.md +237 -0
  33. package/content/knowledge/library/library-requirements.md +173 -0
  34. package/content/knowledge/library/library-security.md +257 -0
  35. package/content/knowledge/library/library-testing.md +319 -0
  36. package/content/knowledge/library/library-type-definitions.md +284 -0
  37. package/content/knowledge/library/library-versioning.md +300 -0
  38. package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
  39. package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
  40. package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
  41. package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
  42. package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
  43. package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
  44. package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
  45. package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
  46. package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
  47. package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
  48. package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
  49. package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
  50. package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
  51. package/content/knowledge/web-app/web-app-architecture.md +116 -0
  52. package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
  53. package/content/knowledge/web-app/web-app-conventions.md +121 -0
  54. package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
  55. package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
  56. package/content/knowledge/web-app/web-app-deployment.md +134 -0
  57. package/content/knowledge/web-app/web-app-design-system.md +158 -0
  58. package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
  59. package/content/knowledge/web-app/web-app-observability.md +221 -0
  60. package/content/knowledge/web-app/web-app-project-structure.md +160 -0
  61. package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
  62. package/content/knowledge/web-app/web-app-requirements.md +112 -0
  63. package/content/knowledge/web-app/web-app-security.md +193 -0
  64. package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
  65. package/content/knowledge/web-app/web-app-testing.md +249 -0
  66. package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
  67. package/content/methodology/backend-overlay.yml +73 -0
  68. package/content/methodology/cli-overlay.yml +69 -0
  69. package/content/methodology/library-overlay.yml +67 -0
  70. package/content/methodology/mobile-app-overlay.yml +71 -0
  71. package/content/methodology/web-app-overlay.yml +79 -0
  72. package/dist/cli/commands/init.d.ts +21 -0
  73. package/dist/cli/commands/init.d.ts.map +1 -1
  74. package/dist/cli/commands/init.js +261 -13
  75. package/dist/cli/commands/init.js.map +1 -1
  76. package/dist/cli/commands/init.test.js +206 -0
  77. package/dist/cli/commands/init.test.js.map +1 -1
  78. package/dist/config/schema.d.ts +1392 -64
  79. package/dist/config/schema.d.ts.map +1 -1
  80. package/dist/config/schema.js +82 -5
  81. package/dist/config/schema.js.map +1 -1
  82. package/dist/config/schema.test.js +302 -1
  83. package/dist/config/schema.test.js.map +1 -1
  84. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  85. package/dist/core/assembly/overlay-loader.js +2 -1
  86. package/dist/core/assembly/overlay-loader.js.map +1 -1
  87. package/dist/core/assembly/overlay-loader.test.js +56 -0
  88. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  89. package/dist/e2e/game-pipeline.test.js +1 -0
  90. package/dist/e2e/game-pipeline.test.js.map +1 -1
  91. package/dist/e2e/project-type-overlays.test.d.ts +16 -0
  92. package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
  93. package/dist/e2e/project-type-overlays.test.js +834 -0
  94. package/dist/e2e/project-type-overlays.test.js.map +1 -0
  95. package/dist/types/config.d.ts +19 -2
  96. package/dist/types/config.d.ts.map +1 -1
  97. package/dist/types/index.d.ts +0 -1
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/dist/types/index.js +0 -1
  100. package/dist/types/index.js.map +1 -1
  101. package/dist/wizard/questions.d.ts +27 -1
  102. package/dist/wizard/questions.d.ts.map +1 -1
  103. package/dist/wizard/questions.js +142 -3
  104. package/dist/wizard/questions.js.map +1 -1
  105. package/dist/wizard/questions.test.js +206 -8
  106. package/dist/wizard/questions.test.js.map +1 -1
  107. package/dist/wizard/wizard.d.ts +21 -0
  108. package/dist/wizard/wizard.d.ts.map +1 -1
  109. package/dist/wizard/wizard.js +27 -1
  110. package/dist/wizard/wizard.js.map +1 -1
  111. package/package.json +1 -1
  112. package/dist/types/wizard.d.ts +0 -14
  113. package/dist/types/wizard.d.ts.map +0 -1
  114. package/dist/types/wizard.js +0 -2
  115. package/dist/types/wizard.js.map +0 -1
@@ -0,0 +1,245 @@
1
+ ---
2
+ name: mobile-app-project-structure
3
+ description: Platform directory layout, shared code modules, asset management, and per-environment configuration for iOS and Android mobile apps
4
+ topics: [mobile-app, project-structure, ios, android, assets, configuration, modules]
5
+ ---
6
+
7
+ Mobile project structure decisions affect build times, code sharing, onboarding velocity, and refactoring safety. iOS and Android have platform-mandated directory conventions that tools expect — deviating from them breaks Xcode file resolution, Android Gradle source sets, and code-generation tooling. Within those constraints, module boundaries and shared-code strategies deserve explicit design.
8
+
9
+ ## Summary
10
+
11
+ iOS projects organize source by feature module under a top-level group matching the app target; Android projects use Gradle modules with `src/main/` source sets per module. Shared business logic lives in a dedicated module (Swift Package, Gradle module, or cross-platform layer). Assets are organized by type and managed through asset catalogs (iOS) or `res/drawable` (Android). Environment configuration uses `.xcconfig` files (iOS) or `BuildConfig` fields in Gradle (Android). Keep feature modules independent — no cross-feature imports.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### iOS Project Structure
16
+
17
+ **Xcode project layout**
18
+
19
+ ```
20
+ MyApp/
21
+ ├── MyApp.xcodeproj/ # Xcode project file (tracked in git)
22
+ ├── MyApp.xcworkspace/ # Workspace if using CocoaPods/SPM (tracked)
23
+ ├── MyApp/ # Main app target
24
+ │ ├── App/ # App entry point
25
+ │ │ ├── MyApp.swift # @main entry point
26
+ │ │ └── AppDelegate.swift # UIApplicationDelegate (if needed)
27
+ │ ├── Features/ # Feature modules (one folder per feature)
28
+ │ │ ├── Auth/
29
+ │ │ │ ├── Views/
30
+ │ │ │ ├── ViewModels/
31
+ │ │ │ ├── Models/
32
+ │ │ │ └── Services/
33
+ │ │ ├── Profile/
34
+ │ │ └── Home/
35
+ │ ├── Core/ # Shared utilities, extensions, base classes
36
+ │ │ ├── Extensions/
37
+ │ │ ├── Utilities/
38
+ │ │ └── Base/
39
+ │ ├── Services/ # App-wide services (networking, analytics, storage)
40
+ │ │ ├── Network/
41
+ │ │ ├── Storage/
42
+ │ │ └── Analytics/
43
+ │ ├── Resources/ # Assets and resources
44
+ │ │ ├── Assets.xcassets # Images, colors, app icon
45
+ │ │ ├── Localizable.strings
46
+ │ │ └── Info.plist
47
+ │ └── Supporting Files/
48
+ │ └── Configuration/ # .xcconfig files
49
+ │ ├── Debug.xcconfig
50
+ │ ├── Release.xcconfig
51
+ │ └── Shared.xcconfig
52
+ ├── MyAppTests/ # Unit test target
53
+ ├── MyAppUITests/ # UI test target
54
+ └── Packages/ # Local Swift packages (optional)
55
+ └── MyAppCore/ # Shared business logic package
56
+ ```
57
+
58
+ **Swift Package Manager for modularization**
59
+ - Local packages declared in the project: `File > Add Package Dependencies > Add Local...`
60
+ - Package.swift defines module boundaries explicitly with product declarations
61
+ - Each local package can export only its public API — private implementation is hidden
62
+ - Move business logic, networking, and data models into packages early; Xcode compilation is parallelized per package
63
+
64
+ **File organization within feature**
65
+ - One file per type: `LoginView.swift`, `LoginViewModel.swift`, `AuthRepository.swift`
66
+ - Group by role, not by type: `Auth/Views/LoginView.swift` not `Views/LoginView.swift`
67
+ - Avoid mega-files. When a file exceeds ~200 lines, consider extraction.
68
+
69
+ ### Android Project Structure
70
+
71
+ **Gradle multi-module layout**
72
+
73
+ ```
74
+ MyApp/
75
+ ├── app/ # Main application module
76
+ │ ├── src/
77
+ │ │ ├── main/
78
+ │ │ │ ├── java/com/example/myapp/
79
+ │ │ │ │ ├── MainActivity.kt
80
+ │ │ │ │ └── MyApplication.kt
81
+ │ │ │ ├── res/
82
+ │ │ │ │ ├── drawable/
83
+ │ │ │ │ ├── layout/ # XML layouts (View system only)
84
+ │ │ │ │ ├── values/
85
+ │ │ │ │ │ ├── colors.xml
86
+ │ │ │ │ │ ├── strings.xml
87
+ │ │ │ │ │ └── themes.xml
88
+ │ │ │ │ └── mipmap/ # App icon (all densities)
89
+ │ │ │ └── AndroidManifest.xml
90
+ │ │ ├── debug/ # Debug-only sources and resources
91
+ │ │ └── release/ # Release-only sources (e.g., no-op analytics)
92
+ │ └── build.gradle.kts
93
+ ├── feature/ # Feature modules
94
+ │ ├── auth/
95
+ │ │ ├── src/main/java/com/example/myapp/feature/auth/
96
+ │ │ │ ├── AuthScreen.kt
97
+ │ │ │ ├── AuthViewModel.kt
98
+ │ │ │ └── AuthRepository.kt
99
+ │ │ └── build.gradle.kts
100
+ │ ├── profile/
101
+ │ └── home/
102
+ ├── core/ # Core shared modules
103
+ │ ├── data/ # Repositories, data sources
104
+ │ ├── domain/ # Use cases, domain models
105
+ │ ├── network/ # Retrofit, OkHttp, interceptors
106
+ │ ├── storage/ # Room, DataStore, Keychain
107
+ │ ├── ui/ # Shared Compose components, theme
108
+ │ └── testing/ # Test utilities and fakes
109
+ ├── build.gradle.kts # Root build file
110
+ ├── settings.gradle.kts # Module declarations
111
+ └── gradle/
112
+ ├── libs.versions.toml # Version catalog
113
+ └── wrapper/
114
+ ```
115
+
116
+ **Dependency rules (enforced with lint or Dependency Guard)**
117
+ - `app` can depend on `feature/*` and `core/*`
118
+ - `feature/*` can depend on `core/*` only — never on other features
119
+ - `core/domain` has no Android dependencies — pure Kotlin
120
+ - `core/data` depends on `core/domain`; `core/network` depends on `core/data`
121
+ - Circular dependencies between modules will break the build — the structure prevents them
122
+
123
+ **Version catalog (`libs.versions.toml`)**
124
+ ```toml
125
+ [versions]
126
+ kotlin = "2.0.0"
127
+ compose-bom = "2024.09.00"
128
+ hilt = "2.51"
129
+
130
+ [libraries]
131
+ compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
132
+ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
133
+
134
+ [plugins]
135
+ android-application = { id = "com.android.application", version = "8.6.0" }
136
+ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
137
+ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
138
+ ```
139
+
140
+ ### Asset Management
141
+
142
+ **iOS Assets.xcassets**
143
+ - All images must be in an asset catalog — never load images from bare file paths at runtime
144
+ - Image sets: provide 1x, 2x, 3x for bitmap assets; prefer PDF or SVG (Universal) for icons and vector art
145
+ - Color sets: define semantic color pairs (light/dark) in the asset catalog, not in code. Name them by semantic role: `PrimaryText`, `Background`, `AccentColor`
146
+ - App Icon set: Xcode 15+ accepts a single 1024×1024 image and generates all sizes automatically
147
+ - Symbol sets: for custom SF-Symbol-style icons, add `.svg` files as symbol sets
148
+ - Namespace asset catalogs using folder names with `.xcassets` grouping: `Icons.xcassets`, `Images.xcassets`, `Colors.xcassets`
149
+
150
+ **Android drawable resources**
151
+ - Vector drawables (`.xml`) for all icons — scale perfectly on any density
152
+ - Bitmap assets: provide `mdpi`, `hdpi`, `xhdpi`, `xxhdpi`, `xxxhdpi` variants in separate `drawable-*` folders, or use Android Studio's vector import to auto-generate
153
+ - Night mode: place dark-mode variants in `drawable-night/` and `values-night/colors.xml`
154
+ - Adaptive icons (`res/mipmap-anydpi-v26/ic_launcher.xml`): required for Android 8.0+; foreground + background layers
155
+ - App icon: 512×512 PNG for Play Store; adaptive icon XML for device display
156
+ - Localized strings: `values/strings.xml` (default), `values-es/strings.xml` (Spanish), etc.
157
+
158
+ ### Environment Configuration
159
+
160
+ **iOS — xcconfig files**
161
+ ```
162
+ # Shared.xcconfig
163
+ BASE_URL = https$(inherited)://api.myapp.com
164
+
165
+ # Debug.xcconfig
166
+ #include "Shared.xcconfig"
167
+ BASE_URL = https://api-dev.myapp.com
168
+ BUNDLE_ID_SUFFIX = .debug
169
+
170
+ # Release.xcconfig
171
+ #include "Shared.xcconfig"
172
+ BASE_URL = https://api.myapp.com
173
+ BUNDLE_ID_SUFFIX =
174
+ ```
175
+
176
+ Access in code via `Info.plist` (read xcconfig variables into plist) then:
177
+ ```swift
178
+ let baseURL = Bundle.main.infoDictionary?["BASE_URL"] as? String ?? ""
179
+ ```
180
+
181
+ **Never hardcode secrets in xcconfig** — these are tracked in git. Use the iOS Keychain for runtime secrets or load from a non-tracked `.env.xcconfig` file that is gitignored.
182
+
183
+ **Android — BuildConfig fields**
184
+ ```kotlin
185
+ // build.gradle.kts (app module)
186
+ android {
187
+ buildTypes {
188
+ debug {
189
+ buildConfigField("String", "BASE_URL", "\"https://api-dev.myapp.com\"")
190
+ applicationIdSuffix = ".debug"
191
+ }
192
+ release {
193
+ buildConfigField("String", "BASE_URL", "\"https://api.myapp.com\"")
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ Access in code: `BuildConfig.BASE_URL`
200
+
201
+ **Product flavors for multi-environment builds**
202
+ ```kotlin
203
+ flavorDimensions += "environment"
204
+ productFlavors {
205
+ create("staging") {
206
+ dimension = "environment"
207
+ buildConfigField("String", "BASE_URL", "\"https://api-staging.myapp.com\"")
208
+ }
209
+ create("production") {
210
+ dimension = "environment"
211
+ buildConfigField("String", "BASE_URL", "\"https://api.myapp.com\"")
212
+ }
213
+ }
214
+ ```
215
+
216
+ **Secrets management**
217
+ - Never commit API keys, signing credentials, or OAuth secrets to git
218
+ - iOS: use `local.xcconfig` (gitignored) that overrides base config, or environment variables in CI
219
+ - Android: `local.properties` (gitignored) for local overrides; CI injects via environment variables
220
+ - At runtime: fetch secrets from a secrets manager (AWS Secrets Manager, HashiCorp Vault) or use platform secure storage (Keychain, Keystore) seeded during onboarding
221
+
222
+ ### Cross-Platform Shared Code
223
+
224
+ **Swift Package for shared logic (iOS only or iOS + macOS)**
225
+ ```swift
226
+ // Package.swift
227
+ let package = Package(
228
+ name: "AppCore",
229
+ platforms: [.iOS(.v16), .macOS(.v13)],
230
+ products: [
231
+ .library(name: "AppCore", targets: ["AppCore"]),
232
+ ],
233
+ targets: [
234
+ .target(name: "AppCore", path: "Sources/AppCore"),
235
+ .testTarget(name: "AppCoreTests", dependencies: ["AppCore"])
236
+ ]
237
+ )
238
+ ```
239
+
240
+ **Kotlin Multiplatform (iOS + Android)**
241
+ - `commonMain`: pure Kotlin business logic, domain models, use cases
242
+ - `iosMain`: iOS-specific implementations of `expect` declarations
243
+ - `androidMain`: Android-specific implementations
244
+ - UI remains platform-native (SwiftUI for iOS, Compose for Android)
245
+ - Data layer: `commonMain` repositories with platform-specific database drivers (`SQLDelight` handles this)
@@ -0,0 +1,321 @@
1
+ ---
2
+ name: mobile-app-push-notifications
3
+ description: APNs and FCM setup, notification channels, rich notifications, background handling, and permission best practices for mobile apps
4
+ topics: [mobile-app, push-notifications, apns, fcm, firebase, notification-channels, rich-notifications, background]
5
+ ---
6
+
7
+ Push notifications are a direct channel to re-engage users, but they are also the fastest way to lose them: irrelevant or excessive notifications get disabled or trigger uninstalls. The technical implementation requires setting up APNs (Apple Push Notification service) for iOS and FCM (Firebase Cloud Messaging) for Android, managing device tokens, and handling notifications in all app states (foreground, background, terminated). Get the permission request timing right — it determines your opt-in rate.
8
+
9
+ ## Summary
10
+
11
+ iOS push requires APNs certificate or key setup, `UNUserNotificationCenter` for permission and handling, and a device token registered with your backend. Android push uses FCM with notification channels (required for Android 8.0+) and `FirebaseMessagingService` for token and message handling. Both platforms support rich notifications (images, actions, custom UI). Permission requests must explain value before asking — iOS grants are permanent opt-outs if declined. Handle notifications in all three app states: foreground, background, and terminated.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### iOS: APNs Setup
16
+
17
+ **APNs authentication options**
18
+
19
+ *APNs Key (recommended)*
20
+ - Create an APNs authentication key in the Apple Developer portal: Certificates, Identifiers & Profiles > Keys
21
+ - Download the `.p8` file — this is valid for all apps and does not expire
22
+ - Key credentials: Key ID (10 characters) + Team ID + `.p8` file
23
+ - Never commit the `.p8` file to git — store in a secrets manager
24
+
25
+ *APNs Certificate (legacy)*
26
+ - Certificate expires annually — requires renewal
27
+ - App-specific — one certificate per bundle ID
28
+ - Use key-based auth for all new integrations
29
+
30
+ **Xcode capability setup**
31
+ 1. Add "Push Notifications" capability in Xcode > Target > Signing & Capabilities
32
+ 2. Add "Background Modes" capability and check "Remote notifications" for silent/background pushes
33
+ 3. Xcode adds the `aps-environment` entitlement automatically
34
+
35
+ **Device token registration (Swift)**
36
+ ```swift
37
+ // AppDelegate or @main App struct
38
+ class AppDelegate: NSObject, UIApplicationDelegate {
39
+ func application(
40
+ _ application: UIApplication,
41
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
42
+ ) {
43
+ let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
44
+ // Send token to your backend
45
+ Task { await PushTokenService.shared.registerToken(token) }
46
+ }
47
+
48
+ func application(
49
+ _ application: UIApplication,
50
+ didFailToRegisterForRemoteNotificationsWithError error: Error
51
+ ) {
52
+ // Simulators always fail here — log but don't treat as fatal in debug
53
+ print("Push registration failed: \(error)")
54
+ }
55
+ }
56
+ ```
57
+
58
+ **Permission request (timing matters)**
59
+ ```swift
60
+ func requestPushPermission() async {
61
+ let center = UNUserNotificationCenter.current()
62
+ let settings = await center.notificationSettings()
63
+ guard settings.authorizationStatus == .notDetermined else { return }
64
+
65
+ // Only ask after providing value context — e.g., after first order placed
66
+ let granted = try? await center.requestAuthorization(options: [.alert, .badge, .sound])
67
+ if granted == true {
68
+ await MainActor.run {
69
+ UIApplication.shared.registerForRemoteNotifications()
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Rules for permission timing:
76
+ - Never ask on first launch — users have not yet experienced value
77
+ - Ask after a meaningful action: first order, first message received, or explicit "Enable notifications" tap in settings UI
78
+ - If denied, guide users to Settings rather than asking again (iOS prevents re-prompting)
79
+
80
+ **Notification handling (all app states)**
81
+ ```swift
82
+ extension AppDelegate: UNUserNotificationCenterDelegate {
83
+ // Foreground: notification arrives while app is active
84
+ func userNotificationCenter(
85
+ _ center: UNUserNotificationCenter,
86
+ willPresent notification: UNNotification,
87
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
88
+ ) {
89
+ // Decide whether to show banner while app is open
90
+ completionHandler([.banner, .sound, .badge])
91
+ }
92
+
93
+ // User tapped notification (foreground or background)
94
+ func userNotificationCenter(
95
+ _ center: UNUserNotificationCenter,
96
+ didReceive response: UNNotificationResponse,
97
+ withCompletionHandler completionHandler: @escaping () -> Void
98
+ ) {
99
+ let userInfo = response.notification.request.content.userInfo
100
+ // Handle deep link or action
101
+ handleNotificationTap(userInfo: userInfo)
102
+ completionHandler()
103
+ }
104
+ }
105
+
106
+ // Silent background notification (content-available: 1)
107
+ func application(
108
+ _ application: UIApplication,
109
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
110
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
111
+ ) {
112
+ Task {
113
+ await SyncEngine.shared.syncOnPushReceived()
114
+ completionHandler(.newData)
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Android: FCM Setup
120
+
121
+ **Firebase project setup**
122
+ 1. Create project in Firebase Console
123
+ 2. Add Android app with the package name
124
+ 3. Download `google-services.json` and place in `app/` directory (not project root)
125
+ 4. Add to `build.gradle.kts`: `apply plugin: "com.google.gms.google-services"`
126
+
127
+ **FCM Service implementation**
128
+ ```kotlin
129
+ @AndroidEntryPoint
130
+ class MyFirebaseMessagingService : FirebaseMessagingService() {
131
+
132
+ @Inject lateinit var pushTokenRepository: PushTokenRepository
133
+ @Inject lateinit var notificationManager: AppNotificationManager
134
+
135
+ // Token refresh — called on first run and when token changes
136
+ override fun onNewToken(token: String) {
137
+ super.onNewToken(token)
138
+ // Send to backend — use WorkManager to ensure delivery even if offline
139
+ pushTokenRepository.scheduleTokenUpload(token)
140
+ }
141
+
142
+ // Data message received (app in foreground or background)
143
+ override fun onMessageReceived(message: RemoteMessage) {
144
+ super.onMessageReceived(message)
145
+ when (message.data["type"]) {
146
+ "chat_message" -> handleChatMessage(message)
147
+ "order_update" -> handleOrderUpdate(message)
148
+ else -> notificationManager.showGenericNotification(message)
149
+ }
150
+ }
151
+
152
+ private fun handleChatMessage(message: RemoteMessage) {
153
+ notificationManager.showChatNotification(
154
+ title = message.data["sender_name"] ?: "New message",
155
+ body = message.data["preview"] ?: "",
156
+ conversationId = message.data["conversation_id"] ?: return
157
+ )
158
+ }
159
+ }
160
+ ```
161
+
162
+ **Notification channels (Android 8.0+ requirement)**
163
+ ```kotlin
164
+ class AppNotificationManager @Inject constructor(
165
+ private val context: Context,
166
+ private val notificationManager: NotificationManagerCompat
167
+ ) {
168
+ companion object {
169
+ const val CHANNEL_CHAT = "chat_messages"
170
+ const val CHANNEL_ORDERS = "order_updates"
171
+ const val CHANNEL_PROMOTIONS = "promotions"
172
+ }
173
+
174
+ fun createChannels() {
175
+ val channels = listOf(
176
+ NotificationChannel(CHANNEL_CHAT, "Chat Messages", NotificationManager.IMPORTANCE_HIGH).apply {
177
+ description = "Messages from other users"
178
+ enableVibration(true)
179
+ },
180
+ NotificationChannel(CHANNEL_ORDERS, "Order Updates", NotificationManager.IMPORTANCE_DEFAULT).apply {
181
+ description = "Updates on your orders"
182
+ },
183
+ NotificationChannel(CHANNEL_PROMOTIONS, "Promotions", NotificationManager.IMPORTANCE_LOW).apply {
184
+ description = "Deals and offers — can be disabled without affecting order updates"
185
+ }
186
+ )
187
+ notificationManager.createNotificationChannels(channels)
188
+ }
189
+ }
190
+ ```
191
+
192
+ Channel importance rules:
193
+ - `IMPORTANCE_HIGH`: chat, time-sensitive alerts — shows heads-up notification
194
+ - `IMPORTANCE_DEFAULT`: order updates, reminders — shows in notification shade
195
+ - `IMPORTANCE_LOW`: promotions, non-urgent — no sound/vibration
196
+ - `IMPORTANCE_MIN`: silent, no icon in status bar — rarely useful
197
+
198
+ Separate channels by user concern, not by technical category. Users can disable individual channels in Settings — respect their choices.
199
+
200
+ **Showing a notification**
201
+ ```kotlin
202
+ fun showChatNotification(title: String, body: String, conversationId: String) {
203
+ val intent = Intent(context, MainActivity::class.java).apply {
204
+ putExtra("destination", "chat/$conversationId")
205
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
206
+ }
207
+ val pendingIntent = PendingIntent.getActivity(
208
+ context, conversationId.hashCode(), intent,
209
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
210
+ )
211
+
212
+ val notification = NotificationCompat.Builder(context, CHANNEL_CHAT)
213
+ .setSmallIcon(R.drawable.ic_notification)
214
+ .setContentTitle(title)
215
+ .setContentText(body)
216
+ .setAutoCancel(true)
217
+ .setContentIntent(pendingIntent)
218
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
219
+ .build()
220
+
221
+ notificationManager.notify(conversationId.hashCode(), notification)
222
+ }
223
+ ```
224
+
225
+ ### Rich Notifications
226
+
227
+ **iOS: Notification Service Extension**
228
+ For modifying notification content before display (decrypting, downloading media):
229
+ 1. Add a Notification Service Extension target in Xcode
230
+ 2. Implement `UNNotificationServiceExtension`:
231
+
232
+ ```swift
233
+ class NotificationService: UNNotificationServiceExtension {
234
+ override func didReceive(
235
+ _ request: UNNotificationRequest,
236
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
237
+ ) {
238
+ let content = request.content.mutableCopy() as! UNMutableNotificationContent
239
+
240
+ // Download and attach image
241
+ if let imageURL = content.userInfo["image_url"] as? String,
242
+ let url = URL(string: imageURL),
243
+ let attachment = try? UNNotificationAttachment(
244
+ identifier: "image",
245
+ url: downloadImage(from: url), // must download synchronously or with semaphore
246
+ options: nil
247
+ ) {
248
+ content.attachments = [attachment]
249
+ }
250
+ contentHandler(content)
251
+ }
252
+ }
253
+ ```
254
+
255
+ **iOS: Notification Content Extension**
256
+ For fully custom notification UI:
257
+ - Add a Notification Content Extension target
258
+ - Set `UNNotificationExtensionCategory` in the Info.plist to the APNs category string
259
+ - Implement `UNNotificationContentExtension` in the ViewController
260
+
261
+ **Android: Big notifications**
262
+ ```kotlin
263
+ // Expandable text
264
+ NotificationCompat.Builder(context, CHANNEL_ORDERS)
265
+ .setStyle(NotificationCompat.BigTextStyle().bigText(longBody))
266
+
267
+ // Image notification
268
+ NotificationCompat.Builder(context, CHANNEL_CHAT)
269
+ .setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap))
270
+
271
+ // Conversation (messaging style — shows thread)
272
+ NotificationCompat.Builder(context, CHANNEL_CHAT)
273
+ .setStyle(NotificationCompat.MessagingStyle("You")
274
+ .addMessage("Hello!", timestamp, sender)
275
+ .addMessage("How are you?", timestamp2, sender)
276
+ )
277
+ ```
278
+
279
+ **Notification actions**
280
+ ```kotlin
281
+ // Android action buttons
282
+ val replyAction = NotificationCompat.Action.Builder(
283
+ R.drawable.ic_reply, "Reply",
284
+ PendingIntent.getBroadcast(context, 0, replyIntent, PendingIntent.FLAG_MUTABLE)
285
+ ).addRemoteInput(
286
+ RemoteInput.Builder("reply_text").setLabel("Reply...").build()
287
+ ).build()
288
+
289
+ NotificationCompat.Builder(context, CHANNEL_CHAT)
290
+ .addAction(replyAction)
291
+ .addAction(R.drawable.ic_mark_read, "Mark as read", markReadPendingIntent)
292
+ ```
293
+
294
+ ### Token Lifecycle Management
295
+
296
+ **Token refresh handling**
297
+ - Both APNs tokens (iOS) and FCM tokens (Android) can change: on reinstall, OS update, or after long inactivity
298
+ - Always store the token server-side with a device identifier
299
+ - Implement token refresh callbacks (`onNewToken` for FCM, `didRegisterForRemoteNotificationsWithDeviceToken` for APNs) and sync to backend
300
+ - Store multiple tokens per user (one per device)
301
+ - Clean up invalid tokens: when sending to a token returns a 404 (APNs) or `InvalidRegistration` (FCM), remove it from your database
302
+
303
+ **Server-side notification delivery**
304
+ ```javascript
305
+ // Firebase Admin SDK
306
+ const message = {
307
+ notification: { title: "New message", body: "Jane says hi" },
308
+ data: { type: "chat_message", conversation_id: "abc123" },
309
+ apns: {
310
+ payload: { aps: { sound: "default", badge: 1 } }
311
+ },
312
+ android: {
313
+ priority: "high",
314
+ notification: { channel_id: "chat_messages" }
315
+ },
316
+ token: deviceToken
317
+ };
318
+ await admin.messaging().send(message);
319
+ ```
320
+
321
+ Always send both `notification` and `data` payloads for maximum compatibility — `notification` payloads are handled by the system when the app is in the background, `data` payloads require app code to handle.