@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,59 @@
1
+ import SwiftUI
2
+
3
+ /// What each version changed, the newest version first, and a pending
4
+ /// section on top when one is being written.
5
+ struct ChangelogPage: View {
6
+ let entries: [ChangelogEntry]
7
+ let isLoading: Bool
8
+ let errorMessage: String?
9
+ let onRefresh: () async -> Void
10
+
11
+ var body: some View {
12
+ Group {
13
+ if isLoading && entries.isEmpty {
14
+ ProgressView()
15
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
16
+ } else if let errorMessage, entries.isEmpty {
17
+ EmptyState.failure("Notes de version indisponibles", message: errorMessage) {
18
+ await onRefresh()
19
+ }
20
+ } else {
21
+ List(entries) { entry in
22
+ Section {
23
+ ForEach(Array(entry.notes.enumerated()), id: \.offset) { _, note in
24
+ Label {
25
+ Text(note)
26
+ } icon: {
27
+ Image(systemName: "circle.fill")
28
+ .font(.system(size: 5))
29
+ .foregroundStyle(.secondary)
30
+ }
31
+ .labelStyle(.caption)
32
+ .font(.callout)
33
+ }
34
+ } header: {
35
+ HStack {
36
+ Text(entry.version)
37
+ Spacer()
38
+ if let date = entry.date {
39
+ Text(date.formatted(date: .abbreviated, time: .omitted))
40
+ } else {
41
+ Text("À venir")
42
+ }
43
+ }
44
+ }
45
+ }
46
+ .listStyle(.insetGrouped)
47
+ .refreshable { await onRefresh() }
48
+ }
49
+ }
50
+ .navigationTitle("Nouveautés")
51
+ .navigationBarTitleDisplayMode(.inline)
52
+ }
53
+ }
54
+
55
+ #Preview {
56
+ NavigationStack {
57
+ ChangelogPage(entries: ChangelogEntry.previews, isLoading: false, errorMessage: nil) {}
58
+ }
59
+ }
@@ -0,0 +1,118 @@
1
+ import SwiftUI
2
+
3
+ /// Lets the user write to us: a problem to report, or an idea for the app.
4
+ /// The message goes straight to Sentry, where the errors already land.
5
+ ///
6
+ /// The send is fire-and-forget: the SDK queues the message and retries on its
7
+ /// own, so there is nothing to await and the confirmation below is optimistic.
8
+ struct FeedbackSheet: View {
9
+ /// Sends the message; `sendFeedback` in the app, nothing in a preview.
10
+ let onSend: (FeedbackKind, String) -> Void
11
+
12
+ @Environment(\.dismiss) private var dismiss
13
+
14
+ @State private var kind: FeedbackKind = .bug
15
+ @State private var message = ""
16
+ @State private var sent = false
17
+
18
+ var body: some View {
19
+ NavigationStack {
20
+ ZStack {
21
+ if sent {
22
+ confirmation
23
+ .transition(.scale(scale: 0.8).combined(with: .opacity))
24
+ } else {
25
+ composer
26
+ .transition(.opacity)
27
+ }
28
+ }
29
+ .animation(.smooth(duration: 0.3), value: sent)
30
+ .sensoryFeedback(.success, trigger: sent)
31
+ .navigationTitle("Nous écrire")
32
+ .navigationBarTitleDisplayMode(.inline)
33
+ .toolbar {
34
+ ToolbarItem(placement: .cancellationAction) {
35
+ ToolbarIconButton(
36
+ title: sent ? "Terminé" : "Annuler",
37
+ systemImage: "xmark",
38
+ role: .cancel
39
+ ) { dismiss() }
40
+ }
41
+ if !sent {
42
+ ToolbarItem(placement: .confirmationAction) {
43
+ ToolbarIconButton(title: "Envoyer", systemImage: "paperplane.fill") { send() }
44
+ .disabled(trimmedMessage.isEmpty)
45
+ .accessibilityIdentifier("feedback-send")
46
+ }
47
+ }
48
+ }
49
+ .task(id: sent) {
50
+ guard sent else { return }
51
+ try? await Task.sleep(for: .seconds(2))
52
+ guard !Task.isCancelled else { return }
53
+ dismiss()
54
+ }
55
+ }
56
+ }
57
+
58
+ private var composer: some View {
59
+ Form {
60
+ // Bare on the page rather than in a card: it sets the subject, it is
61
+ // not a field of the message.
62
+ Picker("Sujet", selection: $kind) {
63
+ Text("Problème").tag(FeedbackKind.bug)
64
+ Text("Suggestion").tag(FeedbackKind.idea)
65
+ }
66
+ .pickerStyle(.segmented)
67
+ .listRowBackground(Color.clear)
68
+ .listRowInsets(EdgeInsets())
69
+
70
+ Section {
71
+ TextField(kindHint, text: $message, axis: .vertical)
72
+ .lineLimit(5...)
73
+ .accessibilityIdentifier("feedback-message")
74
+ }
75
+ }
76
+ }
77
+
78
+ /// Centred confirmation that replaces the form once the message is queued,
79
+ /// then steps aside on its own after a couple of seconds.
80
+ private var confirmation: some View {
81
+ VStack(spacing: 16) {
82
+ Image(systemName: "checkmark.circle.fill")
83
+ .font(.system(size: 76))
84
+ .foregroundStyle(.green)
85
+ .symbolEffect(.bounce, options: .nonRepeating, value: sent)
86
+
87
+ Text("Message envoyé")
88
+ .font(.title3.weight(.semibold))
89
+
90
+ Text("Merci, votre message nous aide à améliorer l'application.")
91
+ .font(.subheadline)
92
+ .foregroundStyle(.secondary)
93
+ .multilineTextAlignment(.center)
94
+ }
95
+ .padding(.horizontal, 40)
96
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
97
+ }
98
+
99
+ private var kindHint: LocalizedStringKey {
100
+ switch kind {
101
+ case .bug: "Décrivez ce que vous faisiez et ce qui s'est passé."
102
+ case .idea: "Dites-nous ce qui manque ou ce qui gagnerait à changer."
103
+ }
104
+ }
105
+
106
+ private var trimmedMessage: String {
107
+ message.trimmingCharacters(in: .whitespacesAndNewlines)
108
+ }
109
+
110
+ private func send() {
111
+ onSend(kind, trimmedMessage)
112
+ sent = true
113
+ }
114
+ }
115
+
116
+ #Preview {
117
+ FeedbackSheet { _, _ in }
118
+ }
@@ -0,0 +1,90 @@
1
+ import SwiftUI
2
+
3
+ /// Everything the profile page can ask for.
4
+ enum ProfileSettingsAction {
5
+ case signOut
6
+ case deleteAccount
7
+ }
8
+
9
+ /// The account: who it is, and the two ways out — signing
10
+ /// out, which keeps everything, and deleting, which keeps nothing.
11
+ struct ProfileSettingsPage: View {
12
+ let firstName: String?
13
+ let displayName: String?
14
+ let email: String?
15
+ /// The first eight characters of the account id: enough to name it in a
16
+ /// support message, short enough to read aloud.
17
+ let shortUid: String
18
+ let isDeletingAccount: Bool
19
+ let errorMessage: String?
20
+ let onAction: (ProfileSettingsAction) async -> Void
21
+
22
+ @State private var showDeleteConfirmation = false
23
+
24
+ var body: some View {
25
+ Form {
26
+ Section {
27
+ if let firstName, !firstName.isEmpty {
28
+ LabeledInfoRow(title: "Prénom", value: firstName, icon: "person.fill")
29
+ }
30
+ if let displayName, !displayName.isEmpty {
31
+ LabeledInfoRow(title: "Nom", value: displayName, icon: "person.text.rectangle.fill")
32
+ }
33
+ if let email, !email.isEmpty {
34
+ LabeledInfoRow(title: "Email", value: email, icon: "envelope.fill")
35
+ }
36
+ LabeledInfoRow(title: "Identifiant", value: shortUid, icon: "key.fill")
37
+ } header: {
38
+ Text("Compte")
39
+ }
40
+
41
+ Section {
42
+ SignOutButton { Task { await onAction(.signOut) } }
43
+ // The dialog hangs off the button that opens it, so it pops out
44
+ // of the row instead of floating mid-screen.
45
+ DeleteAccountButton(isDeleting: isDeletingAccount) {
46
+ showDeleteConfirmation = true
47
+ }
48
+ .confirmationDialog(
49
+ "Supprimer définitivement le compte ?",
50
+ isPresented: $showDeleteConfirmation,
51
+ titleVisibility: .visible
52
+ ) {
53
+ Button("Supprimer mon compte", role: .destructive) {
54
+ Task { await onAction(.deleteAccount) }
55
+ }
56
+ .accessibilityIdentifier("confirm-delete-account")
57
+ Button("Annuler", role: .cancel) {}
58
+ } message: {
59
+ Text(
60
+ "Toutes vos données seront effacées sans possibilité de récupération. Un abonnement éventuel n'est pas résilié et reste à annuler dans l'App Store."
61
+ )
62
+ }
63
+ } footer: {
64
+ if let errorMessage {
65
+ Text(errorMessage).foregroundStyle(.red)
66
+ }
67
+ }
68
+ }
69
+ .navigationTitle("Profil")
70
+ .navigationBarTitleDisplayMode(.inline)
71
+ }
72
+ }
73
+
74
+ #Preview {
75
+ NavigationStack {
76
+ ProfileSettingsPage(
77
+ firstName: "Ada", displayName: "Ada Lovelace", email: "ada@example.com",
78
+ shortUid: "a1b2c3d4", isDeletingAccount: false, errorMessage: nil
79
+ ) { _ in }
80
+ }
81
+ }
82
+
83
+ #Preview("Deleting") {
84
+ NavigationStack {
85
+ ProfileSettingsPage(
86
+ firstName: "Ada", displayName: nil, email: nil, shortUid: "a1b2c3d4",
87
+ isDeletingAccount: true, errorMessage: nil
88
+ ) { _ in }
89
+ }
90
+ }
@@ -0,0 +1,70 @@
1
+ import SwiftUI
2
+
3
+ /// Where a row of the settings leads, pushed by the screen.
4
+ enum SettingsDestination: Hashable {
5
+ case profile
6
+ case changelog
7
+ }
8
+
9
+ /// Everything the settings page can ask for, besides the rows it pushes.
10
+ enum SettingsHomeAction {
11
+ case showFeedback
12
+ }
13
+
14
+ /// The settings: the account, and the app itself. A new section (a subscription,
15
+ /// notifications, a connected service) is added here as its feature ships.
16
+ struct SettingsHomePage: View {
17
+ /// Who the account is, under "Profil".
18
+ let profileSubtitle: String?
19
+ let appVersion: String
20
+ let buildNumber: String
21
+ let onAction: (SettingsHomeAction) -> Void
22
+
23
+ var body: some View {
24
+ List {
25
+ Section {
26
+ NavigationLink(value: SettingsDestination.profile) {
27
+ SettingsRow(
28
+ icon: "person.crop.circle.fill",
29
+ title: "Profil",
30
+ subtitle: profileSubtitle,
31
+ tint: .blue
32
+ )
33
+ }
34
+ .accessibilityIdentifier("settings-profile")
35
+ }
36
+
37
+ Section("Application") {
38
+ NavigationLink(value: SettingsDestination.changelog) {
39
+ SettingsRow(
40
+ icon: "doc.text.fill",
41
+ title: "Version et nouveautés",
42
+ subtitle: "v\(appVersion) (\(buildNumber))",
43
+ tint: .indigo
44
+ )
45
+ }
46
+ .accessibilityIdentifier("settings-changelog")
47
+ Button {
48
+ onAction(.showFeedback)
49
+ } label: {
50
+ SettingsRow(
51
+ icon: "envelope.fill",
52
+ title: "Nous écrire",
53
+ subtitle: String(localized: "Signaler un problème ou proposer une idée"),
54
+ tint: .pink
55
+ )
56
+ }
57
+ .buttonStyle(.plain)
58
+ .accessibilityIdentifier("settings-feedback")
59
+ }
60
+ }
61
+ .navigationTitle("Réglages")
62
+ .navigationBarTitleDisplayMode(.inline)
63
+ }
64
+ }
65
+
66
+ #Preview {
67
+ NavigationStack {
68
+ SettingsHomePage(profileSubtitle: "ada@example.com", appVersion: "1.0", buildNumber: "42") { _ in }
69
+ }
70
+ }
@@ -0,0 +1,14 @@
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
+ <!-- Partial Info.plist, merged with the generated one (GENERATE_INFOPLIST_FILE).
6
+ The system launch screen is only this colour, so the app takes over
7
+ without a flash. -->
8
+ <key>UILaunchScreen</key>
9
+ <dict>
10
+ <key>UIColorName</key>
11
+ <string>LaunchBackground</string>
12
+ </dict>
13
+ </dict>
14
+ </plist>