@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,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
|
+
}
|
|
Binary file
|
package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json
ADDED
|
@@ -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,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
|
+
}
|