@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.
- package/CHANGELOG.md +18 -0
- package/README.md +26 -2
- package/bin/stack.ts +109 -0
- package/hooks/shutdown-simulators-after-push.sh +19 -0
- package/package.json +40 -4
- package/routines/daily-health.md +78 -0
- package/routines/deprecations.md +23 -0
- package/routines/sentry-autofix.md +76 -0
- package/routines/stack-upgrade.md +45 -0
- package/rules/00-collaboration.md +81 -0
- package/rules/01-workflow.md +81 -0
- package/rules/02-commits.md +71 -0
- package/rules/03-code-review.md +25 -0
- package/rules/04-push.md +75 -0
- package/rules/05-release.md +40 -0
- package/rules/06-changelog.md +48 -0
- package/rules/07-appstore.md +77 -0
- package/rules/08-copy.md +52 -0
- package/rules/09-backend.md +132 -0
- package/rules/10-ios.md +206 -0
- package/rules/11-ops.md +114 -0
- package/rules/12-ci.md +32 -0
- package/rules/13-ai.md +71 -0
- package/rules/14-infra.md +90 -0
- package/rules/15-tooling.md +81 -0
- package/rules/16-routines.md +69 -0
- package/rules/17-landing.md +76 -0
- package/skills/appstore/SKILL.md +53 -0
- package/skills/domain/SKILL.md +136 -0
- package/skills/graphql/SKILL.md +83 -0
- package/skills/ios-feature/SKILL.md +94 -0
- package/skills/stack-install/SKILL.md +35 -0
- package/skills/stack-upgrade/SKILL.md +27 -0
- package/src/catalogue.ts +33 -0
- package/src/config.ts +20 -0
- package/src/init.ts +63 -0
- package/src/migrations.ts +72 -0
- package/src/migrations.unit.test.ts +44 -0
- package/src/package.ts +8 -0
- package/src/render.ts +13 -0
- package/src/render.unit.test.ts +17 -0
- package/src/sync.int.test.ts +103 -0
- package/src/sync.ts +161 -0
- package/src/templates.ts +121 -0
- package/src/validate-content.ts +65 -0
- package/templates/backend/.env.example +10 -0
- package/templates/backend/.github/workflows/deploy.yml +151 -0
- package/templates/backend/.github/workflows/schema.yml +59 -0
- package/templates/backend/.github/workflows/test-feat.yml +40 -0
- package/templates/backend/.github/workflows/test-int.yml +40 -0
- package/templates/backend/.github/workflows/test-unit.yml +64 -0
- package/templates/backend/docs/api-evolution.md +67 -0
- package/templates/backend/firebase.json +21 -0
- package/templates/backend/firestore.indexes.json +4 -0
- package/templates/backend/firestore.rules +11 -0
- package/templates/backend/infra/auth.tf +54 -0
- package/templates/backend/infra/backend.tf +6 -0
- package/templates/backend/infra/cicd.tf +63 -0
- package/templates/backend/infra/firebase.tf +26 -0
- package/templates/backend/infra/firestore.tf +53 -0
- package/templates/backend/infra/function.tf +171 -0
- package/templates/backend/infra/main.tf +26 -0
- package/templates/backend/infra/observer.tf +18 -0
- package/templates/backend/infra/outputs.tf +30 -0
- package/templates/backend/infra/project.tf +64 -0
- package/templates/backend/infra/scheduler.tf +28 -0
- package/templates/backend/infra/secrets.tf +44 -0
- package/templates/backend/infra/state.tf +13 -0
- package/templates/backend/infra/terraform.tfvars.example +16 -0
- package/templates/backend/infra/variables.tf +104 -0
- package/templates/backend/infra/versions.tf +30 -0
- package/templates/backend/nitro.config.ts +40 -0
- package/templates/backend/package.json +51 -0
- package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
- package/templates/backend/scripts/deprecations/status.ts +50 -0
- package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
- package/templates/backend/scripts/deprecations/usage.ts +79 -0
- package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
- package/templates/backend/scripts/deprecations.ts +224 -0
- package/templates/backend/scripts/export-graphql-schema.ts +11 -0
- package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
- package/templates/backend/scripts/install-terraform.sh +61 -0
- package/templates/backend/scripts/tf +11 -0
- package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
- package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
- package/templates/backend/server/domain/changelog/primitives.ts +19 -0
- package/templates/backend/server/domain/changelog/query.ts +13 -0
- package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
- package/templates/backend/server/domain/changelog/types.ts +9 -0
- package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
- package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
- package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
- package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
- package/templates/backend/server/domain/shared/language.ts +26 -0
- package/templates/backend/server/domain/shared/page.ts +25 -0
- package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
- package/templates/backend/server/domain/shared/primitives.ts +50 -0
- package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
- package/templates/backend/server/domain/shared/types.ts +14 -0
- package/templates/backend/server/domain/user/command.ts +16 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
- package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
- package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
- package/templates/backend/server/domain/user/query.int.test.ts +49 -0
- package/templates/backend/server/domain/user/query.ts +34 -0
- package/templates/backend/server/domain/user/types.ts +20 -0
- package/templates/backend/server/domain/user/use-case.ts +18 -0
- package/templates/backend/server/middleware/auth.ts +54 -0
- package/templates/backend/server/plugins/00-sentry.ts +74 -0
- package/templates/backend/server/plugins/01-request-log.ts +28 -0
- package/templates/backend/server/plugins/02-graphql.ts +69 -0
- package/templates/backend/server/plugins/03-boot-time.ts +21 -0
- package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
- package/templates/backend/server/routes/app-config.get.ts +15 -0
- package/templates/backend/server/routes/graphql.ts +52 -0
- package/templates/backend/server/system/app-support.ts +17 -0
- package/templates/backend/server/system/config/index.ts +18 -0
- package/templates/backend/server/system/config/primitives.ts +20 -0
- package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
- package/templates/backend/server/system/config/types.ts +8 -0
- package/templates/backend/server/system/firebase.ts +19 -0
- package/templates/backend/server/system/identity.ts +18 -0
- package/templates/backend/server/system/logger.ts +119 -0
- package/templates/backend/server/system/logger.unit.test.ts +109 -0
- package/templates/backend/server/system/migration/migrations/index.ts +6 -0
- package/templates/backend/server/system/migration/primitives.ts +18 -0
- package/templates/backend/server/system/migration/runner.ts +52 -0
- package/templates/backend/server/system/migration/types.ts +16 -0
- package/templates/backend/server/system/read-meter.ts +100 -0
- package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
- package/templates/backend/server/system/request-cache.ts +139 -0
- package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
- package/templates/backend/server/system/secret-box.ts +49 -0
- package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
- package/templates/backend/server/test/bdd.ts +12 -0
- package/templates/backend/server/test/fake-firestore.ts +483 -0
- package/templates/backend/server/utils/apollo.ts +13 -0
- package/templates/backend/server/utils/fetch.ts +5 -0
- package/templates/backend/server/utils/firestore.ts +91 -0
- package/templates/backend/server/utils/input.ts +36 -0
- package/templates/backend/server/utils/input.unit.test.ts +43 -0
- package/templates/backend/server/utils/slug.ts +21 -0
- package/templates/backend/tsconfig.json +16 -0
- package/templates/base/.github/workflows/renovate.yml +17 -0
- package/templates/base/CHANGELOG.fr.md +3 -0
- package/templates/base/CHANGELOG.md +7 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/README.md +12 -0
- package/templates/base/biome.json +47 -0
- package/templates/base/bunfig.toml +2 -0
- package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
- package/templates/base/docs/data-recovery.md +116 -0
- package/templates/base/docs/ideas.md +4 -0
- package/templates/base/project-context.md +43 -0
- package/templates/base/renovate.json +37 -0
- package/templates/ios/.github/workflows/build-ios.yml +168 -0
- package/templates/ios/.github/workflows/ios-upload.yml +308 -0
- package/templates/ios/.github/workflows/release-ios.yml +178 -0
- package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
- package/templates/ios/infra/Gemfile +5 -0
- package/templates/ios/infra/fastlane/Appfile +1 -0
- package/templates/ios/infra/fastlane/Fastfile +54 -0
- package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
- package/templates/ios/infra/ios.tf +28 -0
- package/templates/ios/ios/ExportOptions.plist +26 -0
- package/templates/ios/ios/apollo-codegen-config.json +16 -0
- package/templates/ios/ios/project.yml +94 -0
- package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
- package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
- package/templates/ios/package.merge.json +10 -0
- package/templates/ios/scripts/apollo-codegen.ts +31 -0
- package/templates/ios/scripts/check-ios-components.ts +72 -0
- package/templates/ios/scripts/check-strings.ts +103 -0
- package/templates/ios/scripts/install-device.sh +29 -0
- 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
|
+
}
|
package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift
ADDED
|
@@ -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
|
+
}
|
package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift
ADDED
|
@@ -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>
|