v0.1.0 — Phase β-0 Setup
Repo-Skelett für cards-native, native SwiftUI-Universal-App für Cardecky (mana e.V.). Web-Parität zu cardecky.mana.how. - project.yml mit Bundle ev.mana.cards, ManaSwiftCore-Dep via path - AppConfig: auth.mana.how + cardecky-api.mana.how, Keychain ev.mana.cards - CardsTheme: forest-Werte aus mana/packages/themes/.../forest.css - LoginView (Email/PW gegen mana-auth via ManaCore.AuthClient) - DashboardView als β-1-Placeholder mit cardecky-api-Reachability-Probe - Log unter Subsystem ev.mana.cards - 3 AppConfig-Tests - iOS-Simulator-Build grün Phasen-Plan: mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
28b20cd934
21 changed files with 896 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.DS_Store
|
||||||
|
.build/
|
||||||
|
.swiftpm/
|
||||||
|
DerivedData/
|
||||||
|
Package.resolved
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
# XcodeGen output
|
||||||
|
*.xcodeproj
|
||||||
|
Sources/Resources/Info.plist
|
||||||
|
Sources/Resources/CardsNative.entitlements
|
||||||
10
.swiftformat
Normal file
10
.swiftformat
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
--swiftversion 6.0
|
||||||
|
--indent 4
|
||||||
|
--maxwidth 120
|
||||||
|
--wraparguments before-first
|
||||||
|
--wrapparameters before-first
|
||||||
|
--wrapcollections before-first
|
||||||
|
--commas inline
|
||||||
|
--semicolons never
|
||||||
|
--self remove
|
||||||
|
--importgrouping testable-bottom
|
||||||
26
.swiftlint.yml
Normal file
26
.swiftlint.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
disabled_rules:
|
||||||
|
- todo
|
||||||
|
- trailing_comma
|
||||||
|
|
||||||
|
opt_in_rules:
|
||||||
|
- empty_count
|
||||||
|
- empty_string
|
||||||
|
- explicit_init
|
||||||
|
- first_where
|
||||||
|
- sorted_first_last
|
||||||
|
- toggle_bool
|
||||||
|
|
||||||
|
line_length:
|
||||||
|
warning: 120
|
||||||
|
error: 160
|
||||||
|
ignores_comments: true
|
||||||
|
|
||||||
|
identifier_name:
|
||||||
|
min_length: 2
|
||||||
|
excluded:
|
||||||
|
- id
|
||||||
|
- ok
|
||||||
|
|
||||||
|
included:
|
||||||
|
- Sources
|
||||||
|
- Tests
|
||||||
183
CLAUDE.md
Normal file
183
CLAUDE.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
# CLAUDE.md — cards-native repo
|
||||||
|
|
||||||
|
Guidance für Claude Code in diesem Repository.
|
||||||
|
|
||||||
|
> **Wenn du gerade neu bist:** lies zuerst [`PLAN.md`](PLAN.md) und
|
||||||
|
> `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` (übergeordneter
|
||||||
|
> Greenfield-Plan). Dieses CLAUDE.md ist die Konventions- und
|
||||||
|
> Cross-Repo-Referenz.
|
||||||
|
|
||||||
|
## Was dieses Repo ist
|
||||||
|
|
||||||
|
**Cards Native** — native SwiftUI-Universal-App (iOS / iPadOS / macOS)
|
||||||
|
für **Cardecky**, die Spaced-Repetition-Karten-App des Vereins
|
||||||
|
**mana e.V.** Web-Parität zu `cardecky.mana.how`, plus native iOS-
|
||||||
|
Affordances (Widgets, Notifications, Universal-Links, Pencil).
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTPS/JWT ┌──────────────────┐
|
||||||
|
cards-api ◄───────────── │ cards-native │ SwiftUI
|
||||||
|
cardecky-api.mana.how │ (this repo) │ SwiftData (Cache)
|
||||||
|
│ ev.mana.cards │ WidgetKit (β-6)
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Phase β-0 — Setup (2026-05-12).** Repo-Skelett, ManaCore + ManaTokens
|
||||||
|
als Package-Dependency, Login + Cardecky-API-Reachability-Probe.
|
||||||
|
Phasen β-1 bis β-7 in `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
||||||
|
|
||||||
|
## Leitprinzip: Web-Parität
|
||||||
|
|
||||||
|
Die Web-App auf `cardecky.mana.how` ist Funktions-Referenz. Bei
|
||||||
|
Konflikt zwischen Native und Web → **Web gewinnt**. Native ist
|
||||||
|
Re-Implementation, kein neues Produkt.
|
||||||
|
|
||||||
|
Datenmodell, FSRS-Verhalten, Marketplace-Slugs, Sharing-URLs:
|
||||||
|
identisch zu Web.
|
||||||
|
|
||||||
|
## Architektonische Invarianten
|
||||||
|
|
||||||
|
Beschlossen. Nicht ohne explizite Diskussion antasten.
|
||||||
|
|
||||||
|
1. **Server-authoritative FSRS.** Grading-Calls gehen *immer* an
|
||||||
|
`POST /api/v1/reviews/:cardId/:subIndex/grade`. Kein lokaler
|
||||||
|
ts-fsrs-Port.
|
||||||
|
2. **Offline-Read, Online-Write.** Decks + Due-Cards via SwiftData
|
||||||
|
gecacht (offline sichtbar). Grades werden bei Offline in einer
|
||||||
|
lokalen Queue persistiert und beim Reconnect der Reihe nach
|
||||||
|
abgesendet.
|
||||||
|
3. **mana-auth via ManaCore.** `import ManaCore`,
|
||||||
|
`AuthClient(config: AppConfig.manaAppConfig)`. Eigene
|
||||||
|
Auth-Implementierung ist verboten.
|
||||||
|
4. **Pure SwiftUI.** Keine externen UI-Libraries. AppKit/UIKit nur
|
||||||
|
als Bridge wenn zwingend (z.B. `PencilKit` für Image-Occlusion).
|
||||||
|
5. **Bundle-ID `ev.mana.cards`.** Reverse-Domain mana-ev.ch.
|
||||||
|
Universal-Link-Domain: `cardecky.mana.how`.
|
||||||
|
6. **Cards-Domain-Logik bleibt am Server.** SubIndex-Berechnung für
|
||||||
|
Cloze, Image-Occlusion-Mask-Validation, Content-Hash — alles
|
||||||
|
Server. Native zeigt nur, was vom Server kommt.
|
||||||
|
7. **`forest`-Theme.** Heute lokal in `CardsTheme.swift` nachgebaut
|
||||||
|
(Werte gespiegelt aus `mana/packages/themes/src/variants/forest.css`).
|
||||||
|
Migration auf ManaTokens-Theme-Switch ist Phase ε.
|
||||||
|
8. **Web gewinnt bei Konflikt.** Eleganteres Native-Verhalten geht
|
||||||
|
zuerst in die Web-App, dann nach hier.
|
||||||
|
|
||||||
|
## Konventionen
|
||||||
|
|
||||||
|
- **Swift 6.0**, Strict Concurrency komplett
|
||||||
|
- **iOS 18 / iPadOS 18 / macOS 15** Minimum
|
||||||
|
- **SwiftUI** als einziges UI-Framework
|
||||||
|
- **XcodeGen** als SOT: `project.yml` definiert Targets, Info.plist,
|
||||||
|
Entitlements. `.xcodeproj`, generierte Info.plist und Entitlements
|
||||||
|
sind **nicht** im Git
|
||||||
|
- **SwiftFormat** mit `.swiftformat` (4-space, 120-col, sorted imports)
|
||||||
|
- **SwiftLint** mit `.swiftlint.yml`
|
||||||
|
- **Logging:** App-Subsystem `ev.mana.cards` via
|
||||||
|
`Sources/Core/Telemetry/Log.swift`. ManaCore loggt parallel unter
|
||||||
|
`ev.mana.core`
|
||||||
|
- **Persistenz:** SwiftData für Deck/Card-Cache (ab β-1), JWT im
|
||||||
|
Keychain (über ManaCore)
|
||||||
|
- **Lokalisierung:** DE primary, EN fallback via `Localizable.xcstrings`
|
||||||
|
|
||||||
|
## Cardecky-API-Wire-Format
|
||||||
|
|
||||||
|
Wire-Format gegen `https://cardecky-api.mana.how/api/v1/*`. Quelle der
|
||||||
|
Wahrheit: `../cards/apps/api/src/routes/*.ts`. Bei neuem DTO
|
||||||
|
verifizieren:
|
||||||
|
|
||||||
|
1. Path + Method gegen den Hono-Handler prüfen
|
||||||
|
2. Response-Schema (`zod`) gegen `Codable`-Struct mappen
|
||||||
|
3. snake_case via `CodingKeys`, Optionale Felder explizit `Optional<T>`
|
||||||
|
4. Test-Fixture aus echtem Server-Response in `Tests/UnitTests/`
|
||||||
|
|
||||||
|
## Repo-Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cards-native/
|
||||||
|
├── project.yml XcodeGen-Manifest (SOT)
|
||||||
|
├── PLAN.md Phase-Tracking (gekürzt aus Greenfield-Plan)
|
||||||
|
├── CLAUDE.md dieses File
|
||||||
|
├── README.md
|
||||||
|
├── .swiftformat, .swiftlint.yml
|
||||||
|
├── Sources/
|
||||||
|
│ ├── App/ CardsNativeApp (@main), RootView
|
||||||
|
│ ├── Features/
|
||||||
|
│ │ ├── Account/ LoginView, AccountView (ab β-1)
|
||||||
|
│ │ ├── Decks/ DashboardView (Placeholder), DeckList (β-1)
|
||||||
|
│ │ ├── Study/ (β-2)
|
||||||
|
│ │ ├── Editor/ (β-3)
|
||||||
|
│ │ ├── Marketplace/ (β-5)
|
||||||
|
│ │ ├── Stats/ (β-1)
|
||||||
|
│ │ └── Imports/ (β-3)
|
||||||
|
│ ├── Core/
|
||||||
|
│ │ ├── Auth/ AppConfig (ManaAppConfig-Provider)
|
||||||
|
│ │ ├── API/ CardsAPI (AuthenticatedTransport-Wrapper)
|
||||||
|
│ │ ├── Domain/ (Card-Type-Enums, Rating-Enum — ab β-2)
|
||||||
|
│ │ ├── Storage/ (SwiftData-Models — ab β-1)
|
||||||
|
│ │ ├── Sync/ (ReviewQueue, MediaCache — ab β-2/β-4)
|
||||||
|
│ │ ├── Telemetry/ OSLog (Subsystem ev.mana.cards)
|
||||||
|
│ │ └── Theme/ CardsTheme (forest-Werte)
|
||||||
|
│ ├── Widgets/ (WidgetKit-Extension — ab β-6)
|
||||||
|
│ ├── ShareExtension/ (Save-as-Card — ab β-6)
|
||||||
|
│ └── Resources/
|
||||||
|
│ ├── Assets.xcassets
|
||||||
|
│ ├── Localizable.xcstrings
|
||||||
|
│ ├── Info.plist (generiert, gitignored)
|
||||||
|
│ └── CardsNative.entitlements (generiert, gitignored)
|
||||||
|
├── Tests/
|
||||||
|
│ ├── UnitTests/
|
||||||
|
│ └── UITests/
|
||||||
|
└── docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Cross-Repo-Doks
|
||||||
|
|
||||||
|
- `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — vollständiger
|
||||||
|
Phasen-Plan und Architektur-Entscheidungen
|
||||||
|
- `../mana/docs/MANA_SWIFT.md` — native-Plattform-SOT
|
||||||
|
- `../mana/docs/MANA_AUTH_FEDERATION.md` — Auth-Protokoll, das
|
||||||
|
ManaCore implementiert
|
||||||
|
- `../mana/docs/COMPLIANCE.md` — Telemetrie/Auth/Bezahl-Regeln, gilt
|
||||||
|
auch nativ
|
||||||
|
- `../cards/CLAUDE.md` — Cards-Repo, Web + API
|
||||||
|
- `../cards/STATUS.md` — Web-Phasenstand (Funktions-Referenz)
|
||||||
|
- `../mana-swift-core/CLAUDE.md` — geteilter Code, Konventionen
|
||||||
|
|
||||||
|
## Lokal entwickeln
|
||||||
|
|
||||||
|
**Pre-Requisites:**
|
||||||
|
- Xcode 16+
|
||||||
|
- `brew install xcodegen swiftformat swiftlint`
|
||||||
|
- `../mana-swift-core/` muss als Schwester-Verzeichnis existieren
|
||||||
|
(Package-Dependency via `path: ../mana-swift-core`)
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
```bash
|
||||||
|
xcodegen generate
|
||||||
|
open CardsNative.xcodeproj
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vor jedem Commit:**
|
||||||
|
```bash
|
||||||
|
swiftformat Sources Tests
|
||||||
|
swiftlint --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phasen-Disziplin
|
||||||
|
|
||||||
|
Jede Phase aus dem Greenfield-Plan hat ein verifizierbares
|
||||||
|
Erfolgskriterium. Nicht in die nächste Phase reinarbeiten, bevor
|
||||||
|
die vorherige abgeschlossen ist:
|
||||||
|
|
||||||
|
- β-0: leerer Build + Login + API-Reachability-Probe (**JETZT**)
|
||||||
|
- β-1: Decks-Liste mit SwiftData-Cache
|
||||||
|
- β-2: Study-Loop + Offline-Grade-Queue + Endurance-Test auf realem Gerät
|
||||||
|
- β-3: Card-/Deck-Editor (basic, cloze, typing, multiple-choice)
|
||||||
|
- β-4: Media + image-occlusion + audio-front
|
||||||
|
- β-5: Marketplace + Universal-Links
|
||||||
|
- β-6: Native-Polish (Widgets, Notifications, Share-Extension)
|
||||||
|
- β-7: App-Store-Submission
|
||||||
|
|
||||||
|
Bei Phasen-Wechsel: PLAN.md aktualisieren + Greenfield-Plan abhaken.
|
||||||
63
PLAN.md
Normal file
63
PLAN.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Plan — cards-native (SwiftUI Universal)
|
||||||
|
|
||||||
|
**Stand: 2026-05-12 — Phase β-0 abgeschlossen.** Repo lebt lokal,
|
||||||
|
ManaCore + ManaTokens als Package-Dependency, Login funktioniert,
|
||||||
|
Cardecky-API-Reachability-Probe.
|
||||||
|
|
||||||
|
> **SOT:** `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
||||||
|
> Dieses File ist die App-lokale Status-Spur, das Greenfield-Doc
|
||||||
|
> hat die ganze Architektur-Begründung.
|
||||||
|
|
||||||
|
## Aktueller Stand
|
||||||
|
|
||||||
|
✅ **β-0 — Setup**
|
||||||
|
- Repo-Skelett unter `git.mana.how/till/cards-native`
|
||||||
|
- `project.yml` mit Bundle-ID `ev.mana.cards`, ManaSwiftCore via
|
||||||
|
`path: ../mana-swift-core`
|
||||||
|
- `AppConfig` als `ManaAppConfig`-Provider:
|
||||||
|
- Auth: `https://auth.mana.how`
|
||||||
|
- API: `https://cardecky-api.mana.how`
|
||||||
|
- Keychain-Service: `ev.mana.cards`
|
||||||
|
- `CardsTheme.swift` mit forest-Werten (lokal nachgebaut aus
|
||||||
|
`mana/packages/themes/src/variants/forest.css`)
|
||||||
|
- `LoginView` (Email/PW gegen mana-auth)
|
||||||
|
- `DashboardView` als β-1-Placeholder mit API-Reachability-Indikator
|
||||||
|
- 3 Unit-Tests (AppConfig)
|
||||||
|
- iOS-Simulator-Build grün
|
||||||
|
|
||||||
|
## Phasen (Detail in Greenfield-Plan)
|
||||||
|
|
||||||
|
| Phase | Status | Inhalt |
|
||||||
|
|---|---|---|
|
||||||
|
| β-0 | ✅ 2026-05-12 | Setup, Login, API-Probe |
|
||||||
|
| β-1 | ⏳ | Decks lesen, SwiftData-Cache |
|
||||||
|
| β-2 | — | Study-Loop, Offline-Grade-Queue, Endurance-Test |
|
||||||
|
| β-3 | — | Card-/Deck-Editor (basic, cloze, typing, multiple-choice) |
|
||||||
|
| β-4 | — | Media, image-occlusion (PencilKit), audio-front |
|
||||||
|
| β-5 | — | Marketplace, Universal-Links |
|
||||||
|
| β-6 | — | Native-Polish (Widgets, Notifications, Share-Extension) |
|
||||||
|
| β-7 | — | App-Store-Submission |
|
||||||
|
|
||||||
|
## Nächste Schritte für β-1
|
||||||
|
|
||||||
|
Aus Greenfield-Plan-Sektion "Phase β-1 — Decks lesen":
|
||||||
|
|
||||||
|
1. `Deck`-`Codable`-Struct nach Wire-Format aus
|
||||||
|
`../cards/apps/api/src/routes/decks.ts` + `cards/packages/cards-domain/src/schemas/`
|
||||||
|
2. `CardsAPI.decks() -> [Deck]` mit `GET /api/v1/decks`
|
||||||
|
3. `DeckListView` mit Pull-to-Refresh, Card/Due-Counts
|
||||||
|
4. `CachedDeck` als SwiftData-Model mit `lastFetchedAt`
|
||||||
|
5. Offline-Display bei fehlendem Netz
|
||||||
|
6. Inbox-Banner aus `?forked_from_marketplace=true`-Query
|
||||||
|
|
||||||
|
**Erfolgskriterium:** Web-Account-Decks vollständig in identischer
|
||||||
|
Reihenfolge sichtbar, Pull-to-Refresh aktualisiert Counts.
|
||||||
|
|
||||||
|
## Cross-Refs
|
||||||
|
|
||||||
|
- `../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md` — Greenfield-Plan SOT
|
||||||
|
- `../mana/docs/MANA_SWIFT.md` — Plattform-SOT
|
||||||
|
- `../cards/CLAUDE.md` — Cards-Repo
|
||||||
|
- `../cards/STATUS.md` — Web-Phasenstand (Referenz)
|
||||||
|
- `../mana-swift-core/CLAUDE.md` — ManaCore-Konventionen
|
||||||
|
- `CLAUDE.md` — Repo-Konventionen
|
||||||
36
README.md
Normal file
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# cards-native
|
||||||
|
|
||||||
|
Native SwiftUI-Universal-App (iOS / iPadOS / macOS) für Cardecky —
|
||||||
|
die Spaced-Repetition-Karten-App des Vereins **mana e.V.**
|
||||||
|
|
||||||
|
> **Web-App-Parität.** Die existierende Web-App auf
|
||||||
|
> `cardecky.mana.how` ist Funktions- und Verhaltens-Referenz.
|
||||||
|
> Native bringt kein neues Produkt, sondern die App in einer Form,
|
||||||
|
> die iOS-Hardware besser nutzt.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Phase β-0 — Setup.** Leerer Build, Login funktioniert, Cardecky-API-
|
||||||
|
Reachability-Check. Vollständiger Phasen-Plan in
|
||||||
|
`../mana/docs/playbooks/CARDS_NATIVE_GREENFIELD.md`.
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTPS/JWT ┌──────────────────┐
|
||||||
|
cards-api ◄───────────── │ cards-native │ SwiftUI
|
||||||
|
cardecky-api.mana.how │ ev.mana.cards │ WidgetKit (β-6)
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
▼ ▼
|
||||||
|
ManaCore (Auth, Transport) ManaTokens (Designwerte)
|
||||||
|
git.mana.how/till/mana-swift-core v1.0.0+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lokal entwickeln
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodegen generate
|
||||||
|
open CardsNative.xcodeproj # iPhone-17-Simulator
|
||||||
|
```
|
||||||
|
|
||||||
|
Konventionen, Invarianten, Phasen-Disziplin: [`CLAUDE.md`](CLAUDE.md).
|
||||||
22
Sources/App/CardsNativeApp.swift
Normal file
22
Sources/App/CardsNativeApp.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import ManaCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CardsNativeApp: App {
|
||||||
|
@State private var auth: AuthClient
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let auth = AuthClient(config: AppConfig.manaAppConfig)
|
||||||
|
auth.bootstrap()
|
||||||
|
_auth = State(initialValue: auth)
|
||||||
|
Log.app.info("Cards starting — auth status: \(String(describing: auth.status), privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
RootView()
|
||||||
|
.environment(auth)
|
||||||
|
.tint(CardsTheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Sources/App/RootView.swift
Normal file
18
Sources/App/RootView.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import ManaCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Top-Level-Switch: Login vs Dashboard.
|
||||||
|
/// Ab Phase β-1 wird Dashboard durch eine echte Tab-Bar (Decks / Study /
|
||||||
|
/// Stats / Account) ersetzt.
|
||||||
|
struct RootView: View {
|
||||||
|
@Environment(AuthClient.self) private var auth
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch auth.status {
|
||||||
|
case .signedIn:
|
||||||
|
DashboardView()
|
||||||
|
case .unknown, .signedOut, .signingIn, .error:
|
||||||
|
LoginView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Sources/Core/API/CardsAPI.swift
Normal file
22
Sources/Core/API/CardsAPI.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Foundation
|
||||||
|
import ManaCore
|
||||||
|
|
||||||
|
/// Cards-spezifischer API-Client. Wrapper um `AuthenticatedTransport`
|
||||||
|
/// aus ManaCore, der die Cardecky-Endpoints kennt.
|
||||||
|
///
|
||||||
|
/// In Phase β-0 ist die API leer — Endpoints kommen ab β-1 (Decks),
|
||||||
|
/// β-2 (Reviews), β-3 (Editor), β-4 (Media), β-5 (Marketplace).
|
||||||
|
actor CardsAPI {
|
||||||
|
private let transport: AuthenticatedTransport
|
||||||
|
|
||||||
|
init(auth: AuthClient) {
|
||||||
|
transport = AuthenticatedTransport(baseURL: AppConfig.apiBaseURL, auth: auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health-Probe für β-0 — verifiziert dass cardecky-api erreichbar
|
||||||
|
/// ist und der eigene JWT akzeptiert wird.
|
||||||
|
func healthCheck() async throws -> Bool {
|
||||||
|
let (_, http) = try await transport.request(path: "/healthz")
|
||||||
|
return http.statusCode == 200
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Sources/Core/Auth/AppConfig.swift
Normal file
15
Sources/Core/Auth/AppConfig.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Foundation
|
||||||
|
import ManaCore
|
||||||
|
|
||||||
|
/// App-spezifische Konfiguration für Cards. Implementiert `ManaAppConfig`
|
||||||
|
/// aus ManaCore und ergänzt die Cards-eigene `apiBaseURL` (cardecky-api,
|
||||||
|
/// getrennt von mana-auth).
|
||||||
|
enum AppConfig {
|
||||||
|
static let manaAppConfig: ManaAppConfig = DefaultManaAppConfig(
|
||||||
|
authBaseURL: URL(string: "https://auth.mana.how")!,
|
||||||
|
keychainService: "ev.mana.cards",
|
||||||
|
keychainAccessGroup: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
static let apiBaseURL = URL(string: "https://cardecky-api.mana.how")!
|
||||||
|
}
|
||||||
13
Sources/Core/Telemetry/Log.swift
Normal file
13
Sources/Core/Telemetry/Log.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
/// App-eigene OSLog-Logger unter Subsystem `ev.mana.cards`.
|
||||||
|
/// ManaCore loggt unter `ev.mana.core` parallel — siehe
|
||||||
|
/// `mana-swift-core/Sources/ManaCore/Telemetry/CoreLog.swift`.
|
||||||
|
enum Log {
|
||||||
|
static let app = Logger(subsystem: "ev.mana.cards", category: "app")
|
||||||
|
static let auth = Logger(subsystem: "ev.mana.cards", category: "auth")
|
||||||
|
static let api = Logger(subsystem: "ev.mana.cards", category: "api")
|
||||||
|
static let study = Logger(subsystem: "ev.mana.cards", category: "study")
|
||||||
|
static let sync = Logger(subsystem: "ev.mana.cards", category: "sync")
|
||||||
|
}
|
||||||
99
Sources/Core/Theme/CardsTheme.swift
Normal file
99
Sources/Core/Theme/CardsTheme.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
#if canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
|
private typealias PlatformColorType = UIColor
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
private typealias PlatformColorType = NSColor
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Forest-Theme aus `mana/packages/themes/src/variants/forest.css`.
|
||||||
|
/// Lokal in cards-native nachgebaut, weil ManaTokens v1.0.0 nur den
|
||||||
|
/// Default-Theme (mana-Variant) liefert.
|
||||||
|
///
|
||||||
|
/// Migration auf einen Theme-Switch in ManaTokens ist Phase ε aus
|
||||||
|
/// `mana/docs/MANA_SWIFT.md` — bis dahin lebt forest hier.
|
||||||
|
enum CardsTheme {
|
||||||
|
/// Page-Hintergrund
|
||||||
|
static let background = dynamic(light: (0, 0, 100), dark: (142, 30, 8))
|
||||||
|
|
||||||
|
/// Standard-Text
|
||||||
|
static let foreground = dynamic(light: (142, 30, 12), dark: (142, 15, 95))
|
||||||
|
|
||||||
|
/// Card, Panel, Modal
|
||||||
|
static let surface = dynamic(light: (142, 25, 98), dark: (142, 25, 12))
|
||||||
|
|
||||||
|
/// Hover-State auf Surface
|
||||||
|
static let surfaceHover = dynamic(light: (142, 20, 95), dark: (142, 20, 16))
|
||||||
|
|
||||||
|
/// Disabled-Felder, Skeleton
|
||||||
|
static let muted = dynamic(light: (142, 15, 93), dark: (142, 18, 18))
|
||||||
|
|
||||||
|
/// Sekundär-Text, Placeholder
|
||||||
|
static let mutedForeground = dynamic(light: (142, 10, 42), dark: (142, 12, 65))
|
||||||
|
|
||||||
|
/// Rahmen, Trennlinien
|
||||||
|
static let border = dynamic(light: (142, 15, 88), dark: (142, 18, 22))
|
||||||
|
|
||||||
|
/// Cards-Brand-Grün — Tiefgrün im Light, leuchtender im Dark
|
||||||
|
static let primary = dynamic(light: (142, 76, 28), dark: (142, 71, 45))
|
||||||
|
|
||||||
|
/// Text auf Primary
|
||||||
|
static let primaryForeground = dynamic(light: (0, 0, 100), dark: (142, 30, 8))
|
||||||
|
|
||||||
|
static let error = dynamic(light: (0, 84, 60), dark: (0, 63, 55))
|
||||||
|
static let success = dynamic(light: (142, 71, 45), dark: (142, 71, 45))
|
||||||
|
static let warning = dynamic(light: (38, 92, 50), dark: (48, 96, 53))
|
||||||
|
|
||||||
|
// MARK: - HSL Helper
|
||||||
|
|
||||||
|
private static func dynamic(
|
||||||
|
light: (Double, Double, Double),
|
||||||
|
dark: (Double, Double, Double)
|
||||||
|
) -> Color {
|
||||||
|
let lightColor = fromHSL(light.0, light.1, light.2)
|
||||||
|
let darkColor = fromHSL(dark.0, dark.1, dark.2)
|
||||||
|
|
||||||
|
#if canImport(UIKit)
|
||||||
|
return Color(uiColor: UIColor { trait in
|
||||||
|
trait.userInterfaceStyle == .dark ? darkColor : lightColor
|
||||||
|
})
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
return Color(nsColor: NSColor(name: nil) { appearance in
|
||||||
|
let isDark = appearance.bestMatch(from: [.darkAqua, .vibrantDark]) != nil
|
||||||
|
return isDark ? darkColor : lightColor
|
||||||
|
})
|
||||||
|
#else
|
||||||
|
return Color(red: 0, green: 0, blue: 0)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func fromHSL(_ hue: Double, _ saturation: Double, _ lightness: Double) -> PlatformColorType {
|
||||||
|
let h = hue / 360
|
||||||
|
let s = saturation / 100
|
||||||
|
let l = lightness / 100
|
||||||
|
|
||||||
|
if s == 0 {
|
||||||
|
return PlatformColorType(red: l, green: l, blue: l, alpha: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||||
|
let p = 2 * l - q
|
||||||
|
let r = hueToRGB(p, q, h + 1.0 / 3.0)
|
||||||
|
let g = hueToRGB(p, q, h)
|
||||||
|
let b = hueToRGB(p, q, h - 1.0 / 3.0)
|
||||||
|
|
||||||
|
return PlatformColorType(red: r, green: g, blue: b, alpha: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hueToRGB(_ p: Double, _ q: Double, _ rawT: Double) -> Double {
|
||||||
|
var t = rawT
|
||||||
|
if t < 0 { t += 1 }
|
||||||
|
if t > 1 { t -= 1 }
|
||||||
|
if t < 1.0 / 6.0 { return p + (q - p) * 6 * t }
|
||||||
|
if t < 1.0 / 2.0 { return q }
|
||||||
|
if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6 }
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Sources/Features/Account/LoginView.swift
Normal file
78
Sources/Features/Account/LoginView.swift
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import ManaCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LoginView: View {
|
||||||
|
@Environment(AuthClient.self) private var auth
|
||||||
|
@State private var email = ""
|
||||||
|
@State private var password = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
CardsTheme.background.ignoresSafeArea()
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Text("Cards")
|
||||||
|
.font(.system(size: 48, weight: .bold))
|
||||||
|
.foregroundStyle(CardsTheme.primary)
|
||||||
|
Text("Karteikarten des Vereins mana e.V.")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(CardsTheme.mutedForeground)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
TextField("Email", text: $email)
|
||||||
|
.textContentType(.emailAddress)
|
||||||
|
.keyboardType(.emailAddress)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
|
SecureField("Passwort", text: $password)
|
||||||
|
.textContentType(.password)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task { await auth.signIn(email: email, password: password) }
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
if case .signingIn = auth.status {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(CardsTheme.primaryForeground)
|
||||||
|
}
|
||||||
|
Text("Anmelden")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
.foregroundStyle(CardsTheme.primaryForeground)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
.disabled(isSigningIn || email.isEmpty || password.isEmpty)
|
||||||
|
|
||||||
|
if case let .error(message) = auth.status {
|
||||||
|
Text(message)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(CardsTheme.error)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isSigningIn: Bool {
|
||||||
|
if case .signingIn = auth.status { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
LoginView()
|
||||||
|
.environment(AuthClient(config: AppConfig.manaAppConfig))
|
||||||
|
}
|
||||||
58
Sources/Features/Decks/DashboardView.swift
Normal file
58
Sources/Features/Decks/DashboardView.swift
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import ManaCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Phase β-0-Placeholder. Wird in β-1 durch eine echte Tab-Bar mit
|
||||||
|
/// Decks / Study / Stats / Account ersetzt.
|
||||||
|
struct DashboardView: View {
|
||||||
|
@Environment(AuthClient.self) private var auth
|
||||||
|
@State private var apiReachable: Bool?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
CardsTheme.background.ignoresSafeArea()
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Text("Cards")
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
.foregroundStyle(CardsTheme.primary)
|
||||||
|
|
||||||
|
if let email = auth.currentEmail {
|
||||||
|
Text("Angemeldet als \(email)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(CardsTheme.mutedForeground)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentUnavailableView {
|
||||||
|
Label("β-1 in Vorbereitung", systemImage: "rectangle.stack")
|
||||||
|
.foregroundStyle(CardsTheme.foreground)
|
||||||
|
} description: {
|
||||||
|
Text("Decks- und Study-Views kommen in der nächsten Phase.")
|
||||||
|
.foregroundStyle(CardsTheme.mutedForeground)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let reachable = apiReachable {
|
||||||
|
Label(
|
||||||
|
reachable ? "cardecky-api erreichbar" : "cardecky-api nicht erreichbar",
|
||||||
|
systemImage: reachable ? "checkmark.circle.fill" : "xmark.circle.fill"
|
||||||
|
)
|
||||||
|
.foregroundStyle(reachable ? CardsTheme.success : CardsTheme.error)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Abmelden", role: .destructive) {
|
||||||
|
Task { await auth.signOut() }
|
||||||
|
}
|
||||||
|
.padding(.top, 24)
|
||||||
|
}
|
||||||
|
.padding(32)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
let api = CardsAPI(auth: auth)
|
||||||
|
apiReachable = (try? await api.healthCheck()) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
DashboardView()
|
||||||
|
.environment(AuthClient(config: AppConfig.manaAppConfig))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.231",
|
||||||
|
"green" : "0.502",
|
||||||
|
"red" : "0.106"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Sources/Resources/Assets.xcassets/Contents.json
Normal file
6
Sources/Resources/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Sources/Resources/Localizable.xcstrings
Normal file
5
Sources/Resources/Localizable.xcstrings
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"sourceLanguage" : "de",
|
||||||
|
"strings" : { },
|
||||||
|
"version" : "1.0"
|
||||||
|
}
|
||||||
9
Tests/UITests/CardsNativeUITests.swift
Normal file
9
Tests/UITests/CardsNativeUITests.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class CardsNativeUITests: XCTestCase {
|
||||||
|
func testAppLaunches() throws {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
XCTAssertTrue(app.staticTexts["Cards"].waitForExistence(timeout: 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Tests/UnitTests/CardsNativeTests.swift
Normal file
20
Tests/UnitTests/CardsNativeTests.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Testing
|
||||||
|
@testable import CardsNative
|
||||||
|
|
||||||
|
@Suite("AppConfig")
|
||||||
|
struct AppConfigTests {
|
||||||
|
@Test("Cards-API zeigt auf cardecky-api.mana.how")
|
||||||
|
func apiBaseURLPointsToCardecky() {
|
||||||
|
#expect(AppConfig.apiBaseURL.absoluteString == "https://cardecky-api.mana.how")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Auth zeigt auf auth.mana.how")
|
||||||
|
func authBaseURLPointsToManaAuth() {
|
||||||
|
#expect(AppConfig.manaAppConfig.authBaseURL.absoluteString == "https://auth.mana.how")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Keychain-Service ist ev.mana.cards")
|
||||||
|
func keychainServiceIsAppSpecific() {
|
||||||
|
#expect(AppConfig.manaAppConfig.keychainService == "ev.mana.cards")
|
||||||
|
}
|
||||||
|
}
|
||||||
97
project.yml
Normal file
97
project.yml
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
name: CardsNative
|
||||||
|
|
||||||
|
options:
|
||||||
|
bundleIdPrefix: ev.mana
|
||||||
|
createIntermediateGroups: true
|
||||||
|
deploymentTarget:
|
||||||
|
iOS: "18.0"
|
||||||
|
macOS: "15.0"
|
||||||
|
developmentLanguage: de
|
||||||
|
groupSortPosition: top
|
||||||
|
generateEmptyDirectories: true
|
||||||
|
|
||||||
|
packages:
|
||||||
|
ManaSwiftCore:
|
||||||
|
path: ../mana-swift-core
|
||||||
|
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
SWIFT_VERSION: "6.0"
|
||||||
|
SWIFT_STRICT_CONCURRENCY: complete
|
||||||
|
CURRENT_PROJECT_VERSION: "1"
|
||||||
|
MARKETING_VERSION: "0.1.0"
|
||||||
|
GENERATE_INFOPLIST_FILE: "NO"
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING: "YES"
|
||||||
|
DEAD_CODE_STRIPPING: "YES"
|
||||||
|
CLANG_ENABLE_MODULES: "YES"
|
||||||
|
|
||||||
|
targets:
|
||||||
|
CardsNative:
|
||||||
|
type: application
|
||||||
|
supportedDestinations: [iOS, macOS]
|
||||||
|
dependencies:
|
||||||
|
- package: ManaSwiftCore
|
||||||
|
product: ManaCore
|
||||||
|
- package: ManaSwiftCore
|
||||||
|
product: ManaTokens
|
||||||
|
sources:
|
||||||
|
- path: Sources/App
|
||||||
|
- path: Sources/Features
|
||||||
|
- path: Sources/Core
|
||||||
|
- path: Sources/Resources
|
||||||
|
excludes:
|
||||||
|
- "Info.plist"
|
||||||
|
- "CardsNative.entitlements"
|
||||||
|
info:
|
||||||
|
path: Sources/Resources/Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleShortVersionString: "0.1.0"
|
||||||
|
CFBundleVersion: "1"
|
||||||
|
CFBundleDevelopmentRegion: de
|
||||||
|
CFBundleDisplayName: Cards
|
||||||
|
LSApplicationCategoryType: "public.app-category.education"
|
||||||
|
UILaunchScreen: {}
|
||||||
|
CFBundleURLTypes:
|
||||||
|
- CFBundleURLName: ev.mana.cards
|
||||||
|
CFBundleURLSchemes:
|
||||||
|
- cards
|
||||||
|
ITSAppUsesNonExemptEncryption: false
|
||||||
|
entitlements:
|
||||||
|
path: Sources/Resources/CardsNative.entitlements
|
||||||
|
properties:
|
||||||
|
com.apple.security.app-sandbox: true
|
||||||
|
com.apple.security.network.client: true
|
||||||
|
com.apple.security.files.user-selected.read-write: true
|
||||||
|
keychain-access-groups:
|
||||||
|
- $(AppIdentifierPrefix)ev.mana.cards
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||||
|
ENABLE_PREVIEWS: "YES"
|
||||||
|
|
||||||
|
CardsNativeTests:
|
||||||
|
type: bundle.unit-test
|
||||||
|
supportedDestinations: [iOS, macOS]
|
||||||
|
sources:
|
||||||
|
- Tests/UnitTests
|
||||||
|
dependencies:
|
||||||
|
- target: CardsNative
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards.tests
|
||||||
|
GENERATE_INFOPLIST_FILE: "YES"
|
||||||
|
|
||||||
|
CardsNativeUITests:
|
||||||
|
type: bundle.ui-testing
|
||||||
|
supportedDestinations: [iOS, macOS]
|
||||||
|
sources:
|
||||||
|
- Tests/UITests
|
||||||
|
dependencies:
|
||||||
|
- target: CardsNative
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: ev.mana.cards.uitests
|
||||||
|
GENERATE_INFOPLIST_FILE: "YES"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue