@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.
- package/README.md +127 -12
- package/content/knowledge/backend/backend-api-design.md +103 -0
- package/content/knowledge/backend/backend-architecture.md +100 -0
- package/content/knowledge/backend/backend-async-patterns.md +101 -0
- package/content/knowledge/backend/backend-auth-patterns.md +100 -0
- package/content/knowledge/backend/backend-conventions.md +105 -0
- package/content/knowledge/backend/backend-data-modeling.md +102 -0
- package/content/knowledge/backend/backend-deployment.md +100 -0
- package/content/knowledge/backend/backend-dev-environment.md +102 -0
- package/content/knowledge/backend/backend-observability.md +102 -0
- package/content/knowledge/backend/backend-project-structure.md +100 -0
- package/content/knowledge/backend/backend-requirements.md +103 -0
- package/content/knowledge/backend/backend-security.md +104 -0
- package/content/knowledge/backend/backend-testing.md +101 -0
- package/content/knowledge/backend/backend-worker-patterns.md +100 -0
- package/content/knowledge/cli/cli-architecture.md +101 -0
- package/content/knowledge/cli/cli-conventions.md +117 -0
- package/content/knowledge/cli/cli-dev-environment.md +121 -0
- package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
- package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
- package/content/knowledge/cli/cli-output-patterns.md +107 -0
- package/content/knowledge/cli/cli-project-structure.md +124 -0
- package/content/knowledge/cli/cli-requirements.md +101 -0
- package/content/knowledge/cli/cli-shell-integration.md +130 -0
- package/content/knowledge/cli/cli-testing.md +134 -0
- package/content/knowledge/library/library-api-design.md +306 -0
- package/content/knowledge/library/library-architecture.md +247 -0
- package/content/knowledge/library/library-bundling.md +244 -0
- package/content/knowledge/library/library-conventions.md +229 -0
- package/content/knowledge/library/library-dev-environment.md +220 -0
- package/content/knowledge/library/library-documentation.md +300 -0
- package/content/knowledge/library/library-project-structure.md +237 -0
- package/content/knowledge/library/library-requirements.md +173 -0
- package/content/knowledge/library/library-security.md +257 -0
- package/content/knowledge/library/library-testing.md +319 -0
- package/content/knowledge/library/library-type-definitions.md +284 -0
- package/content/knowledge/library/library-versioning.md +300 -0
- package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
- package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
- package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
- package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
- package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
- package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
- package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
- package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
- package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
- package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
- package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
- package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
- package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
- package/content/knowledge/web-app/web-app-architecture.md +116 -0
- package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
- package/content/knowledge/web-app/web-app-conventions.md +121 -0
- package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
- package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
- package/content/knowledge/web-app/web-app-deployment.md +134 -0
- package/content/knowledge/web-app/web-app-design-system.md +158 -0
- package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
- package/content/knowledge/web-app/web-app-observability.md +221 -0
- package/content/knowledge/web-app/web-app-project-structure.md +160 -0
- package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
- package/content/knowledge/web-app/web-app-requirements.md +112 -0
- package/content/knowledge/web-app/web-app-security.md +193 -0
- package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
- package/content/knowledge/web-app/web-app-testing.md +249 -0
- package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
- package/content/methodology/backend-overlay.yml +73 -0
- package/content/methodology/cli-overlay.yml +69 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/content/methodology/web-app-overlay.yml +79 -0
- package/dist/cli/commands/init.d.ts +21 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +261 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +206 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +1392 -64
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +82 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +302 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +2 -1
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +56 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +16 -0
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +834 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -0
- package/dist/types/config.d.ts +19 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wizard/questions.d.ts +27 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +142 -3
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +206 -8
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +21 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +27 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/wizard.d.ts +0 -14
- package/dist/types/wizard.d.ts.map +0 -1
- package/dist/types/wizard.js +0 -2
- 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.
|