@moifort/stack 0.0.0-stage → 0.1.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 (263) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -2
  3. package/bin/stack.ts +109 -0
  4. package/hooks/shutdown-simulators-after-push.sh +19 -0
  5. package/package.json +40 -4
  6. package/routines/daily-health.md +78 -0
  7. package/routines/deprecations.md +23 -0
  8. package/routines/sentry-autofix.md +76 -0
  9. package/routines/stack-upgrade.md +45 -0
  10. package/rules/00-collaboration.md +81 -0
  11. package/rules/01-workflow.md +81 -0
  12. package/rules/02-commits.md +71 -0
  13. package/rules/03-code-review.md +25 -0
  14. package/rules/04-push.md +75 -0
  15. package/rules/05-release.md +40 -0
  16. package/rules/06-changelog.md +48 -0
  17. package/rules/07-appstore.md +77 -0
  18. package/rules/08-copy.md +52 -0
  19. package/rules/09-backend.md +132 -0
  20. package/rules/10-ios.md +206 -0
  21. package/rules/11-ops.md +114 -0
  22. package/rules/12-ci.md +32 -0
  23. package/rules/13-ai.md +71 -0
  24. package/rules/14-infra.md +90 -0
  25. package/rules/15-tooling.md +81 -0
  26. package/rules/16-routines.md +69 -0
  27. package/rules/17-landing.md +76 -0
  28. package/skills/appstore/SKILL.md +53 -0
  29. package/skills/domain/SKILL.md +136 -0
  30. package/skills/graphql/SKILL.md +83 -0
  31. package/skills/ios-feature/SKILL.md +94 -0
  32. package/skills/stack-install/SKILL.md +35 -0
  33. package/skills/stack-upgrade/SKILL.md +27 -0
  34. package/src/catalogue.ts +33 -0
  35. package/src/config.ts +20 -0
  36. package/src/init.ts +63 -0
  37. package/src/migrations.ts +72 -0
  38. package/src/migrations.unit.test.ts +44 -0
  39. package/src/package.ts +8 -0
  40. package/src/render.ts +13 -0
  41. package/src/render.unit.test.ts +17 -0
  42. package/src/sync.int.test.ts +103 -0
  43. package/src/sync.ts +161 -0
  44. package/src/templates.ts +121 -0
  45. package/src/validate-content.ts +65 -0
  46. package/templates/backend/.env.example +10 -0
  47. package/templates/backend/.github/workflows/deploy.yml +151 -0
  48. package/templates/backend/.github/workflows/schema.yml +59 -0
  49. package/templates/backend/.github/workflows/test-feat.yml +40 -0
  50. package/templates/backend/.github/workflows/test-int.yml +40 -0
  51. package/templates/backend/.github/workflows/test-unit.yml +64 -0
  52. package/templates/backend/docs/api-evolution.md +67 -0
  53. package/templates/backend/firebase.json +21 -0
  54. package/templates/backend/firestore.indexes.json +4 -0
  55. package/templates/backend/firestore.rules +11 -0
  56. package/templates/backend/infra/auth.tf +54 -0
  57. package/templates/backend/infra/backend.tf +6 -0
  58. package/templates/backend/infra/cicd.tf +63 -0
  59. package/templates/backend/infra/firebase.tf +26 -0
  60. package/templates/backend/infra/firestore.tf +53 -0
  61. package/templates/backend/infra/function.tf +171 -0
  62. package/templates/backend/infra/main.tf +26 -0
  63. package/templates/backend/infra/observer.tf +18 -0
  64. package/templates/backend/infra/outputs.tf +30 -0
  65. package/templates/backend/infra/project.tf +64 -0
  66. package/templates/backend/infra/scheduler.tf +28 -0
  67. package/templates/backend/infra/secrets.tf +44 -0
  68. package/templates/backend/infra/state.tf +13 -0
  69. package/templates/backend/infra/terraform.tfvars.example +16 -0
  70. package/templates/backend/infra/variables.tf +104 -0
  71. package/templates/backend/infra/versions.tf +30 -0
  72. package/templates/backend/nitro.config.ts +40 -0
  73. package/templates/backend/package.json +51 -0
  74. package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
  75. package/templates/backend/scripts/deprecations/status.ts +50 -0
  76. package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
  77. package/templates/backend/scripts/deprecations/usage.ts +79 -0
  78. package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
  79. package/templates/backend/scripts/deprecations.ts +224 -0
  80. package/templates/backend/scripts/export-graphql-schema.ts +11 -0
  81. package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
  82. package/templates/backend/scripts/install-terraform.sh +61 -0
  83. package/templates/backend/scripts/tf +11 -0
  84. package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
  85. package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
  86. package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
  87. package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
  88. package/templates/backend/server/domain/changelog/primitives.ts +19 -0
  89. package/templates/backend/server/domain/changelog/query.ts +13 -0
  90. package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
  91. package/templates/backend/server/domain/changelog/types.ts +9 -0
  92. package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
  93. package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
  94. package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
  95. package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
  96. package/templates/backend/server/domain/shared/language.ts +26 -0
  97. package/templates/backend/server/domain/shared/page.ts +25 -0
  98. package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
  99. package/templates/backend/server/domain/shared/primitives.ts +50 -0
  100. package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
  101. package/templates/backend/server/domain/shared/types.ts +14 -0
  102. package/templates/backend/server/domain/user/command.ts +16 -0
  103. package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
  104. package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
  105. package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
  106. package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
  107. package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
  108. package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
  109. package/templates/backend/server/domain/user/query.int.test.ts +49 -0
  110. package/templates/backend/server/domain/user/query.ts +34 -0
  111. package/templates/backend/server/domain/user/types.ts +20 -0
  112. package/templates/backend/server/domain/user/use-case.ts +18 -0
  113. package/templates/backend/server/middleware/auth.ts +54 -0
  114. package/templates/backend/server/plugins/00-sentry.ts +74 -0
  115. package/templates/backend/server/plugins/01-request-log.ts +28 -0
  116. package/templates/backend/server/plugins/02-graphql.ts +69 -0
  117. package/templates/backend/server/plugins/03-boot-time.ts +21 -0
  118. package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
  119. package/templates/backend/server/routes/app-config.get.ts +15 -0
  120. package/templates/backend/server/routes/graphql.ts +52 -0
  121. package/templates/backend/server/system/app-support.ts +17 -0
  122. package/templates/backend/server/system/config/index.ts +18 -0
  123. package/templates/backend/server/system/config/primitives.ts +20 -0
  124. package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
  125. package/templates/backend/server/system/config/types.ts +8 -0
  126. package/templates/backend/server/system/firebase.ts +19 -0
  127. package/templates/backend/server/system/identity.ts +18 -0
  128. package/templates/backend/server/system/logger.ts +119 -0
  129. package/templates/backend/server/system/logger.unit.test.ts +109 -0
  130. package/templates/backend/server/system/migration/migrations/index.ts +6 -0
  131. package/templates/backend/server/system/migration/primitives.ts +18 -0
  132. package/templates/backend/server/system/migration/runner.ts +52 -0
  133. package/templates/backend/server/system/migration/types.ts +16 -0
  134. package/templates/backend/server/system/read-meter.ts +100 -0
  135. package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
  136. package/templates/backend/server/system/request-cache.ts +139 -0
  137. package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
  138. package/templates/backend/server/system/secret-box.ts +49 -0
  139. package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
  140. package/templates/backend/server/test/bdd.ts +12 -0
  141. package/templates/backend/server/test/fake-firestore.ts +483 -0
  142. package/templates/backend/server/utils/apollo.ts +13 -0
  143. package/templates/backend/server/utils/fetch.ts +5 -0
  144. package/templates/backend/server/utils/firestore.ts +91 -0
  145. package/templates/backend/server/utils/input.ts +36 -0
  146. package/templates/backend/server/utils/input.unit.test.ts +43 -0
  147. package/templates/backend/server/utils/slug.ts +21 -0
  148. package/templates/backend/tsconfig.json +16 -0
  149. package/templates/base/.github/workflows/renovate.yml +17 -0
  150. package/templates/base/CHANGELOG.fr.md +3 -0
  151. package/templates/base/CHANGELOG.md +7 -0
  152. package/templates/base/CLAUDE.md +34 -0
  153. package/templates/base/README.md +12 -0
  154. package/templates/base/biome.json +47 -0
  155. package/templates/base/bunfig.toml +2 -0
  156. package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
  157. package/templates/base/docs/data-recovery.md +116 -0
  158. package/templates/base/docs/ideas.md +4 -0
  159. package/templates/base/project-context.md +43 -0
  160. package/templates/base/renovate.json +37 -0
  161. package/templates/ios/.github/workflows/build-ios.yml +168 -0
  162. package/templates/ios/.github/workflows/ios-upload.yml +308 -0
  163. package/templates/ios/.github/workflows/release-ios.yml +178 -0
  164. package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
  165. package/templates/ios/infra/Gemfile +5 -0
  166. package/templates/ios/infra/fastlane/Appfile +1 -0
  167. package/templates/ios/infra/fastlane/Fastfile +54 -0
  168. package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
  169. package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
  170. package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
  171. package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
  172. package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
  173. package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
  174. package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
  175. package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
  176. package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
  177. package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
  178. package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
  179. package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
  180. package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
  181. package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
  182. package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
  183. package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
  184. package/templates/ios/infra/ios.tf +28 -0
  185. package/templates/ios/ios/ExportOptions.plist +26 -0
  186. package/templates/ios/ios/apollo-codegen-config.json +16 -0
  187. package/templates/ios/ios/project.yml +94 -0
  188. package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
  189. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  190. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  191. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
  192. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  193. package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
  194. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
  195. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
  196. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
  197. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
  198. package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
  199. package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
  200. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
  201. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
  202. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
  203. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
  204. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
  205. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
  206. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
  207. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
  208. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
  209. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
  210. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
  211. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
  212. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
  213. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
  214. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
  215. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
  216. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
  217. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
  218. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
  219. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
  220. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
  221. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
  222. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
  223. package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
  224. package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
  225. package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
  226. package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
  227. package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
  228. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
  229. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
  230. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
  231. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
  232. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
  233. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
  234. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
  235. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
  236. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
  237. package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
  238. package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
  239. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
  240. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
  241. package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
  242. package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
  243. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
  244. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
  245. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
  246. package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
  247. package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
  248. package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
  249. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
  250. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
  251. package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
  252. package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
  253. package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
  254. package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
  255. package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
  256. package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
  257. package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
  258. package/templates/ios/package.merge.json +10 -0
  259. package/templates/ios/scripts/apollo-codegen.ts +31 -0
  260. package/templates/ios/scripts/check-ios-components.ts +72 -0
  261. package/templates/ios/scripts/check-strings.ts +103 -0
  262. package/templates/ios/scripts/install-device.sh +29 -0
  263. package/templates/ios/scripts/testflight.ts +213 -0
@@ -0,0 +1,28 @@
1
+ resource "google_firebase_apple_app" "ios" {
2
+ provider = google-beta
3
+ project = google_project.this.project_id
4
+ display_name = "{{APP_NAME}} iOS"
5
+ bundle_id = "{{BUNDLE_ID}}"
6
+ team_id = var.apple_team_id
7
+
8
+ depends_on = [google_firebase_project.this]
9
+ }
10
+
11
+ data "google_firebase_apple_app_config" "ios" {
12
+ provider = google-beta
13
+ project = google_project.this.project_id
14
+ app_id = google_firebase_apple_app.ios.app_id
15
+ }
16
+
17
+ # Written next to the app for a local build; gitignored. CI rebuilds it from the
18
+ # IOS_GOOGLE_SERVICE_INFO_PLIST secret.
19
+ resource "local_file" "google_service_info_plist" {
20
+ filename = "${path.root}/../ios/{{APP_NAME}}/GoogleService-Info.plist"
21
+ content_base64 = data.google_firebase_apple_app_config.ios.config_file_contents
22
+ file_permission = "0644"
23
+ }
24
+
25
+ output "ios_app_id" {
26
+ value = google_firebase_apple_app.ios.app_id
27
+ description = "Firebase iOS app id"
28
+ }
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App Store Connect distribution with a dedicated (non-managed) certificate.
6
+ Manual signing avoids Xcode cloud signing, which cannot use managed
7
+ distribution certificates from a fresh CI machine. -->
8
+ <key>method</key>
9
+ <string>app-store-connect</string>
10
+ <key>teamID</key>
11
+ <string>{{APPLE_TEAM_ID}}</string>
12
+ <key>signingStyle</key>
13
+ <string>manual</string>
14
+ <key>signingCertificate</key>
15
+ <string>Apple Distribution</string>
16
+ <key>provisioningProfiles</key>
17
+ <dict>
18
+ <key>{{BUNDLE_ID}}</key>
19
+ <string>{{APP_NAME}} App Store</string>
20
+ </dict>
21
+ <key>uploadSymbols</key>
22
+ <true/>
23
+ <key>destination</key>
24
+ <string>export</string>
25
+ </dict>
26
+ </plist>
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaNamespace": "{{APP_NAME}}GraphQL",
3
+ "input": {
4
+ "schemaSearchPaths": ["../shared/schema.graphql"],
5
+ "operationSearchPaths": ["{{APP_NAME}}/Features/**/GraphQL/*.graphql"]
6
+ },
7
+ "output": {
8
+ "schemaTypes": {
9
+ "path": "{{APP_NAME}}/Generated/GraphQL",
10
+ "moduleType": { "embeddedInTarget": { "name": "{{APP_NAME}}" } }
11
+ },
12
+ "operations": { "inSchemaModule": {} },
13
+ "testMocks": { "none": {} }
14
+ },
15
+ "options": { "markTypesNonisolated": true }
16
+ }
@@ -0,0 +1,94 @@
1
+ # The Xcode project is generated from this file: `bun run ios:project` (XcodeGen).
2
+ # Never add a file through Xcode's UI: folders are synchronised, so a new file on
3
+ # disk is in the target. Edit this file for settings, packages and targets.
4
+ name: "{{APP_NAME}}"
5
+ options:
6
+ bundleIdPrefix: com.polyforms
7
+ deploymentTarget:
8
+ iOS: "26.0"
9
+ defaultConfig: Debug
10
+ developmentLanguage: en
11
+
12
+ settings:
13
+ base:
14
+ SWIFT_VERSION: "6.0"
15
+ SWIFT_STRICT_CONCURRENCY: complete
16
+ DEVELOPMENT_TEAM: "{{APPLE_TEAM_ID}}"
17
+ MARKETING_VERSION: "1.0"
18
+ CURRENT_PROJECT_VERSION: 1
19
+ DEVELOPMENT_LANGUAGE: en
20
+ LOCALIZATION_PREFERS_STRING_CATALOGS: YES
21
+ SWIFT_EMIT_LOC_STRINGS: YES
22
+
23
+ packages:
24
+ Apollo:
25
+ url: https://github.com/apollographql/apollo-ios.git
26
+ from: "2.2.0"
27
+ Firebase:
28
+ url: https://github.com/firebase/firebase-ios-sdk.git
29
+ from: "13.0.0"
30
+ Sentry:
31
+ url: https://github.com/getsentry/sentry-cocoa.git
32
+ from: "9.0.0"
33
+
34
+ targets:
35
+ "{{APP_NAME}}":
36
+ type: application
37
+ platform: iOS
38
+ sources:
39
+ - path: "{{APP_NAME}}"
40
+ type: syncedFolder
41
+ dependencies:
42
+ - package: Apollo
43
+ product: Apollo
44
+ - package: Firebase
45
+ product: FirebaseCore
46
+ - package: Firebase
47
+ product: FirebaseAuth
48
+ - package: Sentry
49
+ product: Sentry
50
+ settings:
51
+ base:
52
+ PRODUCT_BUNDLE_IDENTIFIER: "{{BUNDLE_ID}}"
53
+ INFOPLIST_FILE: "{{APP_NAME}}/Info.plist"
54
+ GENERATE_INFOPLIST_FILE: YES
55
+ INFOPLIST_KEY_CFBundleDisplayName: "{{APP_NAME}}"
56
+ INFOPLIST_KEY_UISupportedInterfaceOrientations: UIInterfaceOrientationPortrait
57
+ INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
58
+ ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
59
+ CODE_SIGN_ENTITLEMENTS: "{{APP_NAME}}/{{APP_NAME}}.entitlements"
60
+ CODE_SIGN_STYLE: Automatic
61
+ configs:
62
+ Release:
63
+ # The archive signs manually with the distribution certificate and the
64
+ # App Store profile the upload workflow installs.
65
+ CODE_SIGN_STYLE: Manual
66
+ CODE_SIGN_IDENTITY: Apple Distribution
67
+ PROVISIONING_PROFILE_SPECIFIER: "{{APP_NAME}} App Store"
68
+
69
+ "{{APP_NAME}}Tests":
70
+ type: bundle.unit-test
71
+ platform: iOS
72
+ sources:
73
+ - path: "{{APP_NAME}}Tests"
74
+ type: syncedFolder
75
+ dependencies:
76
+ - target: "{{APP_NAME}}"
77
+ settings:
78
+ base:
79
+ PRODUCT_BUNDLE_IDENTIFIER: "{{BUNDLE_ID}}.tests"
80
+ GENERATE_INFOPLIST_FILE: YES
81
+ TEST_HOST: "$(BUILT_PRODUCTS_DIR)/{{APP_NAME}}.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/{{APP_NAME}}"
82
+ BUNDLE_LOADER: "$(TEST_HOST)"
83
+
84
+ schemes:
85
+ "{{APP_NAME}}":
86
+ build:
87
+ targets:
88
+ "{{APP_NAME}}": all
89
+ "{{APP_NAME}}Tests": [test]
90
+ test:
91
+ targets:
92
+ - "{{APP_NAME}}Tests"
93
+ archive:
94
+ config: Release
@@ -0,0 +1,46 @@
1
+ import FirebaseCore
2
+ import Sentry
3
+ import SwiftUI
4
+
5
+ @main
6
+ struct App: SwiftUI.App {
7
+ init() {
8
+ FirebaseApp.configure()
9
+ #if DEBUG
10
+ // Points Auth at the local emulator and signs in, only when the UI test
11
+ // launch arguments are there. A normal Debug run is untouched.
12
+ UITestEnvironment.bootstrapIfNeeded()
13
+ #endif
14
+ startSentry()
15
+ }
16
+
17
+ var body: some Scene {
18
+ WindowGroup {
19
+ AuthRoot()
20
+ }
21
+ }
22
+
23
+ private func startSentry() {
24
+ #if DEBUG
25
+ // Debug builds (Xcode, simulator) never start Sentry, so their noise stays
26
+ // out of the production project. Archived builds report.
27
+ #else
28
+ let dsn = Secrets.sentryDsn
29
+ if dsn.isEmpty { return }
30
+
31
+ SentrySDK.start { options in
32
+ options.dsn = dsn
33
+ // Errors are never sampled, only performance traces are.
34
+ options.tracesSampleRate = 0.1
35
+ options.enableAutoSessionTracking = true
36
+ options.enableTimeToFullDisplayTracing = true
37
+ // MetricKit hands over the system's own diagnostics: hangs, and the
38
+ // watchdog kills with the stack they happened on.
39
+ options.enableMetricKit = true
40
+ // Trace headers on our GraphQL calls only, so an app error and the
41
+ // backend error that caused it land in one trace.
42
+ options.tracePropagationTargets = [APIClient.shared.baseURL.absoluteString]
43
+ }
44
+ #endif
45
+ }
46
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "filename" : "AppIcon.png",
5
+ "idiom" : "universal",
6
+ "platform" : "ios",
7
+ "size" : "1024x1024"
8
+ }
9
+ ],
10
+ "info" : {
11
+ "author" : "xcode",
12
+ "version" : 1
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "info" : {
3
+ "author" : "xcode",
4
+ "version" : 1
5
+ }
6
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "colors" : [
3
+ {
4
+ "color" : {
5
+ "color-space" : "srgb",
6
+ "components" : {
7
+ "alpha" : "1.000",
8
+ "blue" : "0.860",
9
+ "green" : "0.930",
10
+ "red" : "0.960"
11
+ }
12
+ },
13
+ "idiom" : "universal"
14
+ },
15
+ {
16
+ "appearances" : [
17
+ {
18
+ "appearance" : "luminosity",
19
+ "value" : "dark"
20
+ }
21
+ ],
22
+ "color" : {
23
+ "color-space" : "srgb",
24
+ "components" : {
25
+ "alpha" : "1.000",
26
+ "blue" : "0.080",
27
+ "green" : "0.090",
28
+ "red" : "0.110"
29
+ }
30
+ },
31
+ "idiom" : "universal"
32
+ }
33
+ ],
34
+ "info" : {
35
+ "author" : "xcode",
36
+ "version" : 1
37
+ }
38
+ }
@@ -0,0 +1,17 @@
1
+ import SwiftUI
2
+
3
+ /// The app once signed in and onboarded: the tabs. Each tab is a feature's
4
+ /// Screen; a new feature adds its tab here.
5
+ struct ContentView: View {
6
+ var body: some View {
7
+ TabView {
8
+ Tab("Accueil", systemImage: "house") {
9
+ HomeScreen()
10
+ }
11
+ }
12
+ }
13
+ }
14
+
15
+ #Preview {
16
+ ContentView()
17
+ }
@@ -0,0 +1,33 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ /// Helpers for the Sign in with Apple nonce required by Firebase Auth.
5
+ enum AppleNonce {
6
+ /// 32-character URL-safe nonce (uppercase letters, digits, hyphen, underscore).
7
+ static func random(length: Int = 32) -> String {
8
+ precondition(length > 0)
9
+ let charset: [Character] = Array(
10
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._"
11
+ )
12
+ var result = ""
13
+ var remaining = length
14
+ while remaining > 0 {
15
+ var random: UInt8 = 0
16
+ let status = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
17
+ guard status == errSecSuccess else { continue }
18
+ if random < charset.count {
19
+ result.append(charset[Int(random)])
20
+ remaining -= 1
21
+ }
22
+ }
23
+ return result
24
+ }
25
+
26
+ /// SHA-256 hex digest used as the value sent to Apple.
27
+ static func sha256(_ input: String) -> String {
28
+ SHA256
29
+ .hash(data: Data(input.utf8))
30
+ .compactMap { String(format: "%02x", $0) }
31
+ .joined()
32
+ }
33
+ }
@@ -0,0 +1,74 @@
1
+ import FirebaseCore
2
+ import SwiftUI
3
+
4
+ /// Top-level gate: the blocking update screen when the backend no longer supports
5
+ /// this build; otherwise the login when no Firebase user is signed in; once signed
6
+ /// in, the onboarding until it is completed, then the app.
7
+ struct AuthRoot: View {
8
+ @State private var session = AuthSession()
9
+ @State private var gate = OnboardingGate()
10
+ @State private var supportGate = AppSupportGate()
11
+ @Environment(\.scenePhase) private var scenePhase
12
+
13
+ var body: some View {
14
+ Group {
15
+ if case .updateRequired(let appStoreURL) = supportGate.state {
16
+ UpdateRequiredView(appStoreURL: appStoreURL)
17
+ } else if isShowcase || session.user != nil {
18
+ signedIn
19
+ } else {
20
+ LoginScreen()
21
+ }
22
+ }
23
+ .environment(session)
24
+ .environment(\.accountFirstName, gate.firstName)
25
+ .task {
26
+ guard !isShowcase else { return }
27
+ await supportGate.check()
28
+ }
29
+ .onChange(of: scenePhase) { _, phase in
30
+ if phase == .active, !isShowcase { Task { await supportGate.check() } }
31
+ }
32
+ .task(id: session.user?.uid) {
33
+ if session.user != nil || isShowcase {
34
+ await gate.refresh()
35
+ } else {
36
+ gate.reset()
37
+ }
38
+ }
39
+ }
40
+
41
+ /// The App Store captures: signed in as the showcase user, without Apple.
42
+ private var isShowcase: Bool {
43
+ #if DEBUG
44
+ Showcase.isOn
45
+ #else
46
+ false
47
+ #endif
48
+ }
49
+
50
+ @ViewBuilder
51
+ private var signedIn: some View {
52
+ switch gate.state {
53
+ case .loading:
54
+ StartupLoadingView()
55
+ case .required:
56
+ OnboardingScreen(onCompleted: { gate.markCompleted(firstName: $0) })
57
+ case .ready:
58
+ ContentView()
59
+ case .failed(let message):
60
+ ContentUnavailableView {
61
+ Label("Connexion impossible", systemImage: "wifi.exclamationmark")
62
+ } description: {
63
+ Text(message)
64
+ } actions: {
65
+ AsyncButton("Réessayer") { await gate.refresh() }
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ #Preview {
72
+ if FirebaseApp.app() == nil { FirebaseApp.configure() }
73
+ return AuthRoot()
74
+ }
@@ -0,0 +1,86 @@
1
+ import FirebaseAuth
2
+ import Observation
3
+ import Sentry
4
+
5
+ /// Single source of truth for the signed-in Firebase user. Lives at app scope;
6
+ /// views that react to sign-in and sign-out observe `user`.
7
+ @MainActor
8
+ @Observable
9
+ final class AuthSession {
10
+ // Qualified: importing Sentry brings its own `User` into scope.
11
+ private(set) var user: FirebaseAuth.User?
12
+ @ObservationIgnored
13
+ nonisolated(unsafe) private var handle: AuthStateDidChangeListenerHandle?
14
+ /// The given name Apple handed over at sign-in. Apple sends it only on the
15
+ /// very first authorization, so it is kept the moment it arrives.
16
+ private var appleGivenName: String?
17
+
18
+ /// The first name to propose in onboarding: Apple's given name when this
19
+ /// sign-in carried one, else the one Firebase kept in the display name.
20
+ var suggestedFirstName: String? {
21
+ if let appleGivenName { return appleGivenName }
22
+ guard let displayName = user?.displayName, !displayName.isEmpty else { return nil }
23
+ let givenName = (try? PersonNameComponents(displayName, strategy: .name))?.givenName
24
+ return givenName?.isEmpty == false ? givenName : nil
25
+ }
26
+
27
+ init() {
28
+ user = Auth.auth().currentUser
29
+ report(user)
30
+ handle = Auth.auth().addStateDidChangeListener { [weak self] _, user in
31
+ Task { @MainActor in
32
+ guard let self else { return }
33
+ // However the session ended (sign-out, account deleted, a dead
34
+ // session dropped by the API client), the next user starts clean.
35
+ if self.user != nil, user == nil { self.forgetAccount() }
36
+ self.user = user
37
+ self.report(user)
38
+ }
39
+ }
40
+ }
41
+
42
+ /// Names the account behind a crash report: the uid only, no email, no name.
43
+ private func report(_ user: FirebaseAuth.User?) {
44
+ SentrySDK.setUser(user.map { Sentry.User(userId: $0.uid) })
45
+ }
46
+
47
+ deinit {
48
+ if let handle { Auth.auth().removeStateDidChangeListener(handle) }
49
+ }
50
+
51
+ func rememberAppleName(_ fullName: PersonNameComponents?) {
52
+ let givenName = fullName?.givenName?.trimmingCharacters(in: .whitespacesAndNewlines)
53
+ appleGivenName = givenName?.isEmpty == false ? givenName : nil
54
+ }
55
+
56
+ func signOut() throws {
57
+ try Auth.auth().signOut()
58
+ }
59
+
60
+ /// Whoever signs in next must not open on this account's data: every cache
61
+ /// the app keeps on disk is cleared here.
62
+ private func forgetAccount() {
63
+ appleGivenName = nil
64
+ SnapshotCaches.clear()
65
+ }
66
+
67
+ /// Signs out when Firebase no longer knows the signed-in account (deleted,
68
+ /// disabled): the ID token outlives the account by up to an hour. Returns
69
+ /// whether it did.
70
+ @discardableResult
71
+ nonisolated static func dropSessionIfAccountIsGone() async -> Bool {
72
+ guard let user = Auth.auth().currentUser else { return false }
73
+ do {
74
+ try await user.reload()
75
+ return false
76
+ } catch {
77
+ let nsError = error as NSError
78
+ guard nsError.domain == AuthErrorDomain,
79
+ let code = AuthErrorCode(rawValue: nsError.code),
80
+ [.userNotFound, .userDisabled, .userTokenExpired, .invalidUserToken].contains(code)
81
+ else { return false }
82
+ await MainActor.run { _ = try? Auth.auth().signOut() }
83
+ return true
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,83 @@
1
+ import AuthenticationServices
2
+ import FirebaseAuth
3
+ import FirebaseCore
4
+ import SwiftUI
5
+
6
+ struct LoginScreen: View {
7
+ @Environment(AuthSession.self) private var session
8
+ @State private var nonce = ""
9
+ @State private var error: String?
10
+ @State private var isSigningIn = false
11
+
12
+ var body: some View {
13
+ VStack(spacing: 32) {
14
+ Spacer()
15
+ VStack(spacing: 12) {
16
+ Image(systemName: "sparkles")
17
+ .font(.system(size: 64))
18
+ .foregroundStyle(.tint)
19
+ .padding(.bottom, 4)
20
+ Text(verbatim: "{{APP_NAME}}")
21
+ .font(.largeTitle.bold())
22
+ Text("Connectez-vous pour retrouver vos données.")
23
+ .font(.subheadline)
24
+ .foregroundStyle(.secondary)
25
+ .multilineTextAlignment(.center)
26
+ }
27
+ Spacer()
28
+ SignInWithAppleButton(.signIn) { request in
29
+ let raw = AppleNonce.random()
30
+ nonce = raw
31
+ request.requestedScopes = [.fullName, .email]
32
+ request.nonce = AppleNonce.sha256(raw)
33
+ } onCompletion: { result in
34
+ Task { await handle(result) }
35
+ }
36
+ .signInWithAppleButtonStyle(.black)
37
+ .frame(height: 50)
38
+ .padding(.horizontal, 32)
39
+ .disabled(isSigningIn)
40
+ if let error {
41
+ Text(error)
42
+ .font(.footnote)
43
+ .foregroundStyle(.red)
44
+ .padding(.horizontal, 32)
45
+ }
46
+ Spacer().frame(height: 40)
47
+ }
48
+ }
49
+
50
+ private func handle(_ result: Result<ASAuthorization, Error>) async {
51
+ isSigningIn = true
52
+ defer { isSigningIn = false }
53
+ do {
54
+ let auth = try result.get()
55
+ guard
56
+ let credential = auth.credential as? ASAuthorizationAppleIDCredential,
57
+ let tokenData = credential.identityToken,
58
+ let token = String(data: tokenData, encoding: .utf8)
59
+ else {
60
+ error = String(localized: "Apple n'a pas renvoyé de jeton.")
61
+ return
62
+ }
63
+ let oauth = OAuthProvider.appleCredential(
64
+ withIDToken: token,
65
+ rawNonce: nonce,
66
+ fullName: credential.fullName
67
+ )
68
+ // Before signing in: the session flips to signed-in the moment
69
+ // Firebase answers, and onboarding reads the name on appearing.
70
+ session.rememberAppleName(credential.fullName)
71
+ _ = try await Auth.auth().signIn(with: oauth)
72
+ } catch {
73
+ self.error = (error as NSError).localizedDescription
74
+ }
75
+ }
76
+ }
77
+
78
+ #Preview {
79
+ // `AuthSession` reads `Auth.auth()` on init, which traps when Firebase was
80
+ // never configured: the canvas does not run `App.init`.
81
+ if FirebaseApp.app() == nil { FirebaseApp.configure() }
82
+ return LoginScreen().environment(AuthSession())
83
+ }
@@ -0,0 +1,21 @@
1
+ import SwiftUI
2
+
3
+ /// The first tab, connected: owns the settings sheet. The first feature of the
4
+ /// app replaces the page's empty state with what it shows.
5
+ struct HomeScreen: View {
6
+ @Environment(\.accountFirstName) private var firstName
7
+ @State private var settingsShown = false
8
+
9
+ var body: some View {
10
+ NavigationStack {
11
+ HomePage(firstName: firstName) { action in
12
+ switch action {
13
+ case .openSettings: settingsShown = true
14
+ }
15
+ }
16
+ .sheet(isPresented: $settingsShown) {
17
+ SettingsHomeScreen()
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,40 @@
1
+ import SwiftUI
2
+
3
+ enum HomeAction {
4
+ case openSettings
5
+ }
6
+
7
+ /// The first tab, pure: a greeting and, until the first feature exists, the empty
8
+ /// state. Settings sit behind the gear, top right.
9
+ struct HomePage: View {
10
+ let firstName: String?
11
+ let onAction: (HomeAction) -> Void
12
+
13
+ var body: some View {
14
+ EmptyState(
15
+ systemImage: "sparkles",
16
+ title: "Rien pour l'instant",
17
+ message: "Ce qui arrive dans l'application s'affichera ici."
18
+ )
19
+ .navigationTitle(title)
20
+ .toolbar {
21
+ ToolbarItem(placement: .primaryAction) {
22
+ ToolbarIconButton(title: "Réglages", systemImage: "gearshape") { onAction(.openSettings) }
23
+ .accessibilityIdentifier("home-settings")
24
+ }
25
+ }
26
+ }
27
+
28
+ private var title: String {
29
+ if let firstName, !firstName.isEmpty { return String(localized: "Bonjour \(firstName)") }
30
+ return String(localized: "Accueil")
31
+ }
32
+ }
33
+
34
+ #Preview {
35
+ NavigationStack { HomePage(firstName: "Ada") { _ in } }
36
+ }
37
+
38
+ #Preview("Without a name") {
39
+ NavigationStack { HomePage(firstName: nil) { _ in } }
40
+ }
@@ -0,0 +1,7 @@
1
+ import SwiftUI
2
+
3
+ extension EnvironmentValues {
4
+ /// The first name the user gave at onboarding, set at the root from the launch
5
+ /// request, so the profile settings show it without a request of their own.
6
+ @Entry var accountFirstName: String? = nil
7
+ }
@@ -0,0 +1,16 @@
1
+ # Everything a signed-in launch needs, in one request: the onboarding or the app,
2
+ # and the first name the profile settings show.
3
+ query Launch {
4
+ me {
5
+ firstName
6
+ onboardingCompleted
7
+ }
8
+ }
9
+
10
+ mutation CompleteOnboarding($input: CompleteOnboardingInput!) {
11
+ completeOnboarding(input: $input) {
12
+ userId
13
+ firstName
14
+ onboardingCompleted
15
+ }
16
+ }