v1.0.0 (3) — icône SVG signature (encre grainée, KA lime ombre décalée, 12 pastilles d univers), connexion KA ID NATIVE (ASWebAuthenticationSession → hub client ka-ios → échange vérifié api-ka, carte de membre), contenus enrichis (photos + descriptions dans listes et fiches), corrections plist (orientations, iPhone seul, chiffrement exempt) ; builds 2 et 3 téléversées sur TestFlight
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 changed files +395 −25
modified
KA/Core/Ecosystem.swift
+12 −1
@@ -60,6 +60,8 @@ struct KAItem: Identifiable, Hashable, Codable { | ||
| 60 | 60 | var imageURL: URL? |
| 61 | 61 | var latitude: Double? |
| 62 | 62 | var longitude: Double? |
| 63 | + /// Description longue (fiche) | |
| 64 | + var detail: String? | |
| 63 | 65 | /// Paires libres affichées sur la fiche (« Année : 2021 », « Salaire : 65 000 $ »…) |
| 64 | 66 | var facts: [Fact] |
| 65 | 67 | |
@@ -111,6 +113,14 @@ enum Ecosystem { | ||
| 111 | 113 | private static func base(_ o: [String: JSONValue], universe: String, idKeys: [String] = ["uid"]) -> KAItem { |
| 112 | 114 | var id: String? |
| 113 | 115 | for k in idKeys { if let v = o[k]?.text { id = v; break } } |
| 116 | + // première image utilisable : champ `images` (liste) ou `image` | |
| 117 | + var image: URL? | |
| 118 | + if let arr = o["images"]?.array { | |
| 119 | + image = arr.compactMap { $0.text }.first(where: { $0.hasPrefix("http") }).flatMap(URL.init(string:)) | |
| 120 | + } | |
| 121 | + if image == nil { | |
| 122 | + image = o.str("image", "image_url", "photo", "thumbnail").flatMap(URL.init(string:)) | |
| 123 | + } | |
| 114 | 124 | return KAItem( |
| 115 | 125 | id: "\(universe):\(id ?? UUID().uuidString)", |
| 116 | 126 | universeID: universe, |
@@ -118,9 +128,10 @@ enum Ecosystem { | ||
| 118 | 128 | subtitle: nil, priceLabel: nil, |
| 119 | 129 | city: o.str("city"), |
| 120 | 130 | url: o.str("url").flatMap(URL.init(string:)), |
| 121 | − imageURL: o.str("image", "image_url", "photo", "thumbnail").flatMap(URL.init(string:)), | |
| 131 | + imageURL: image, | |
| 122 | 132 | latitude: o.num("lat", "latitude"), |
| 123 | 133 | longitude: o.num("lng", "lon", "longitude"), |
| 134 | + detail: o.str("description", "menu_summary", "bio").map { $0.strippingHTML.trimmingCharacters(in: .whitespacesAndNewlines) }, | |
| 124 | 135 | facts: [] |
| 125 | 136 | ) |
| 126 | 137 | } |
added
KA/Core/KAID.swift
+167 −0
@@ -0,0 +1,167 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// KAID.swift — connexion KA ID NATIVE : ASWebAuthenticationSession vers le hub | |
| 3 | +// groupe-ka.com (SSO officiel de l'écosystème, client « ka-ios »), retour par | |
| 4 | +// le scheme ka-ios://auth, puis ÉCHANGE VÉRIFIÉ côté serveur (api-ka valide la | |
| 5 | +// signature du jeton — aucun secret dans l'app) qui renvoie le profil. | |
| 6 | +import AuthenticationServices | |
| 7 | +import SwiftUI | |
| 8 | + | |
| 9 | +struct KAIDProfile: Codable, Equatable { | |
| 10 | + var kaID: String | |
| 11 | + var name: String? | |
| 12 | + var email: String? | |
| 13 | + var picture: String? | |
| 14 | + var roleLabel: String? | |
| 15 | + var city: String? | |
| 16 | + var bio: String? | |
| 17 | +} | |
| 18 | + | |
| 19 | +@MainActor | |
| 20 | +final class KAIDManager: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding { | |
| 21 | + static let shared = KAIDManager() | |
| 22 | + @Published var profile: KAIDProfile? | |
| 23 | + @Published var busy = false | |
| 24 | + @Published var lastError: String? | |
| 25 | + | |
| 26 | + private let storeKey = "ka.id.profile" | |
| 27 | + private var session: ASWebAuthenticationSession? | |
| 28 | + | |
| 29 | + override init() { | |
| 30 | + super.init() | |
| 31 | + if let data = UserDefaults.standard.data(forKey: storeKey), | |
| 32 | + let p = try? JSONDecoder().decode(KAIDProfile.self, from: data) { | |
| 33 | + profile = p | |
| 34 | + } | |
| 35 | + } | |
| 36 | + | |
| 37 | + func login() { | |
| 38 | + guard !busy else { return } | |
| 39 | + busy = true | |
| 40 | + lastError = nil | |
| 41 | + let state = UUID().uuidString | |
| 42 | + var comps = URLComponents(string: "https://www.groupe-ka.com/sso/authorize")! | |
| 43 | + comps.queryItems = [ | |
| 44 | + .init(name: "client_id", value: "ka-ios"), | |
| 45 | + .init(name: "redirect_uri", value: "ka-ios://auth/callback"), | |
| 46 | + .init(name: "state", value: state), | |
| 47 | + ] | |
| 48 | + let s = ASWebAuthenticationSession(url: comps.url!, callbackURLScheme: "ka-ios") { [weak self] url, error in | |
| 49 | + Task { @MainActor in | |
| 50 | + guard let self else { return } | |
| 51 | + defer { self.busy = false } | |
| 52 | + guard error == nil, let url else { | |
| 53 | + if let e = error as? ASWebAuthenticationSessionError, e.code == .canceledLogin { return } | |
| 54 | + self.lastError = "Connexion annulée ou impossible." | |
| 55 | + return | |
| 56 | + } | |
| 57 | + let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] | |
| 58 | + guard items.first(where: { $0.name == "state" })?.value == state, | |
| 59 | + let token = items.first(where: { $0.name == "ka_token" })?.value else { | |
| 60 | + self.lastError = "Réponse du hub invalide." | |
| 61 | + return | |
| 62 | + } | |
| 63 | + await self.exchange(token) | |
| 64 | + } | |
| 65 | + } | |
| 66 | + s.presentationContextProvider = self | |
| 67 | + s.prefersEphemeralWebBrowserSession = false // garde la session hub (Google/courriel) | |
| 68 | + session = s | |
| 69 | + s.start() | |
| 70 | + } | |
| 71 | + | |
| 72 | + private func exchange(_ token: String) async { | |
| 73 | + do { | |
| 74 | + var req = URLRequest(url: URL(string: "https://www.api-ka.com/api/ios/auth/exchange")!) | |
| 75 | + req.httpMethod = "POST" | |
| 76 | + req.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
| 77 | + req.httpBody = try JSONSerialization.data(withJSONObject: ["ka_token": token]) | |
| 78 | + let (data, resp) = try await URLSession.shared.data(for: req) | |
| 79 | + guard (resp as? HTTPURLResponse)?.statusCode == 200, | |
| 80 | + let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any], | |
| 81 | + let kaID = obj["ka_id"] as? String else { | |
| 82 | + lastError = "Vérification du compte impossible." | |
| 83 | + return | |
| 84 | + } | |
| 85 | + let hub = obj["profile"] as? [String: Any] | |
| 86 | + let p = KAIDProfile( | |
| 87 | + kaID: kaID, | |
| 88 | + name: obj["name"] as? String ?? hub?["name"] as? String, | |
| 89 | + email: obj["email"] as? String, | |
| 90 | + picture: obj["picture"] as? String ?? hub?["picture"] as? String, | |
| 91 | + roleLabel: hub?["role_label"] as? String, | |
| 92 | + city: hub?["city"] as? String, | |
| 93 | + bio: hub?["bio"] as? String | |
| 94 | + ) | |
| 95 | + profile = p | |
| 96 | + if let d = try? JSONEncoder().encode(p) { | |
| 97 | + UserDefaults.standard.set(d, forKey: storeKey) | |
| 98 | + } | |
| 99 | + Haptics.success() | |
| 100 | + } catch { | |
| 101 | + lastError = "Réseau indisponible — réessayez." | |
| 102 | + } | |
| 103 | + } | |
| 104 | + | |
| 105 | + func logout() { | |
| 106 | + profile = nil | |
| 107 | + UserDefaults.standard.removeObject(forKey: storeKey) | |
| 108 | + Haptics.tap() | |
| 109 | + } | |
| 110 | + | |
| 111 | + nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { | |
| 112 | + MainActor.assumeIsolated { | |
| 113 | + UIApplication.shared.connectedScenes | |
| 114 | + .compactMap { ($0 as? UIWindowScene)?.keyWindow } | |
| 115 | + .first ?? ASPresentationAnchor() | |
| 116 | + } | |
| 117 | + } | |
| 118 | +} | |
| 119 | + | |
| 120 | +// MARK: - Carte de membre Groupe KA | |
| 121 | + | |
| 122 | +struct KAIDCard: View { | |
| 123 | + let profile: KAIDProfile | |
| 124 | + var body: some View { | |
| 125 | + VStack(alignment: .leading, spacing: 10) { | |
| 126 | + HStack { | |
| 127 | + Text("Groupe").font(.system(.subheadline, design: .rounded).weight(.bold)) | |
| 128 | + .foregroundStyle(Color(hex: "#f5f3ee")) | |
| 129 | + Text("KA").font(.system(.caption, design: .rounded).weight(.bold)) | |
| 130 | + .foregroundStyle(KATheme.lime) | |
| 131 | + .padding(.horizontal, 6).padding(.vertical, 2) | |
| 132 | + .background(Color(hex: "#f5f3ee").opacity(0.14), in: RoundedRectangle(cornerRadius: 6)) | |
| 133 | + Spacer() | |
| 134 | + Text("MEMBRE") | |
| 135 | + .font(.system(size: 9, design: .monospaced).weight(.bold)) | |
| 136 | + .foregroundStyle(KATheme.lime) | |
| 137 | + } | |
| 138 | + HStack(spacing: 12) { | |
| 139 | + AsyncImage(url: profile.picture.flatMap(URL.init(string:))) { phase in | |
| 140 | + if case .success(let img) = phase { img.resizable() } | |
| 141 | + else { KATheme.lime.opacity(0.25).overlay( | |
| 142 | + Text(String(profile.name?.prefix(1) ?? "K")).font(.title2.weight(.bold)).foregroundStyle(KATheme.lime)) } | |
| 143 | + } | |
| 144 | + .frame(width: 54, height: 54) | |
| 145 | + .clipShape(Circle()) | |
| 146 | + .overlay(Circle().strokeBorder(KATheme.lime, lineWidth: 1.6)) | |
| 147 | + VStack(alignment: .leading, spacing: 2) { | |
| 148 | + Text(profile.name ?? "Membre KA") | |
| 149 | + .font(.headline).foregroundStyle(Color(hex: "#f5f3ee")) | |
| 150 | + Text(profile.kaID) | |
| 151 | + .font(.system(.caption, design: .monospaced).weight(.bold)) | |
| 152 | + .foregroundStyle(KATheme.lime) | |
| 153 | + if let r = profile.roleLabel ?? profile.city { | |
| 154 | + Text(r).font(.caption2).foregroundStyle(Color(hex: "#f5f3ee").opacity(0.6)) | |
| 155 | + } | |
| 156 | + } | |
| 157 | + Spacer() | |
| 158 | + } | |
| 159 | + Text("Un seul compte pour les 13 plateformes de l'écosystème.") | |
| 160 | + .font(.caption2).foregroundStyle(Color(hex: "#f5f3ee").opacity(0.55)) | |
| 161 | + } | |
| 162 | + .padding(16) | |
| 163 | + .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) | |
| 164 | + .overlay(RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| 165 | + .strokeBorder(KATheme.lime.opacity(0.5), lineWidth: 1.2)) | |
| 166 | + } | |
| 167 | +} | |
modified
KA/Design/Theme.swift
+22 −4
@@ -128,10 +128,28 @@ struct KAItemRow: View { | ||
| 128 | 128 | |
| 129 | 129 | var body: some View { |
| 130 | 130 | HStack(alignment: .top, spacing: 12) { |
| 131 | − RoundedRectangle(cornerRadius: 8, style: .continuous) | |
| 132 | − .fill(universe?.accent.opacity(0.9) ?? .gray) | |
| 133 | − .frame(width: 5) | |
| 134 | − .padding(.vertical, 2) | |
| 131 | + if let img = item.imageURL { | |
| 132 | + AsyncImage(url: img) { phase in | |
| 133 | + switch phase { | |
| 134 | + case .success(let image): | |
| 135 | + image.resizable().aspectRatio(contentMode: .fill) | |
| 136 | + default: | |
| 137 | + (universe?.accent ?? .gray).opacity(0.12) | |
| 138 | + .overlay(Image(systemName: universe?.symbol ?? "photo") | |
| 139 | + .foregroundStyle(universe?.accent ?? .gray)) | |
| 140 | + } | |
| 141 | + } | |
| 142 | + .frame(width: 74, height: 74) | |
| 143 | + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) | |
| 144 | + .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous) | |
| 145 | + .strokeBorder(.primary.opacity(0.25), lineWidth: 1)) | |
| 146 | + .accessibilityHidden(true) | |
| 147 | + } else { | |
| 148 | + RoundedRectangle(cornerRadius: 8, style: .continuous) | |
| 149 | + .fill(universe?.accent.opacity(0.9) ?? .gray) | |
| 150 | + .frame(width: 5) | |
| 151 | + .padding(.vertical, 2) | |
| 152 | + } | |
| 135 | 153 | VStack(alignment: .leading, spacing: 4) { |
| 136 | 154 | Text(item.title) |
| 137 | 155 | .font(.headline) |
modified
KA/Features/ProfileView.swift
+41 −15
@@ -7,6 +7,7 @@ struct ProfileView: View { | ||
| 7 | 7 | @AppStorage("ka.favUniverses") private var favUniversesRaw = "" |
| 8 | 8 | @AppStorage("ka.appearance") private var appearance = "auto" |
| 9 | 9 | @Environment(\.colorScheme) private var scheme |
| 10 | + @StateObject private var kaid = KAIDManager.shared | |
| 10 | 11 | |
| 11 | 12 | private var favIDs: Set<String> { |
| 12 | 13 | Set(favUniversesRaw.split(separator: ",").map(String.init)) |
@@ -16,23 +17,48 @@ struct ProfileView: View { | ||
| 16 | 17 | NavigationStack { |
| 17 | 18 | List { |
| 18 | 19 | Section { |
| 19 | − HStack(spacing: 14) { | |
| 20 | − ZStack { | |
| 21 | − RoundedRectangle(cornerRadius: 14, style: .continuous) | |
| 22 | − .fill(KATheme.inkLight) | |
| 23 | − .frame(width: 56, height: 56) | |
| 24 | − Text("KA").font(.system(size: 22, weight: .bold, design: .rounded)) | |
| 25 | − .foregroundStyle(KATheme.lime) | |
| 26 | − .rotationEffect(.degrees(-4)) | |
| 20 | + if let p = kaid.profile { | |
| 21 | + KAIDCard(profile: p) | |
| 22 | + .listRowInsets(EdgeInsets()) | |
| 23 | + .listRowBackground(Color.clear) | |
| 24 | + Link(destination: Ecosystem.hubURL.appendingPathComponent("/compte")) { | |
| 25 | + Label("Gérer mon profil sur groupe-ka.com", systemImage: "person.text.rectangle") | |
| 27 | 26 | } |
| 28 | − VStack(alignment: .leading, spacing: 3) { | |
| 29 | − Text("Votre KA ID").font(.headline) | |
| 30 | − Text("Un seul compte pour tout l'écosystème — création et gestion sur groupe-ka.com.") | |
| 31 | − .font(.caption).foregroundStyle(.secondary) | |
| 27 | + Button(role: .destructive) { kaid.logout() } label: { | |
| 28 | + Label("Se déconnecter", systemImage: "rectangle.portrait.and.arrow.right") | |
| 29 | + } | |
| 30 | + } else { | |
| 31 | + HStack(spacing: 14) { | |
| 32 | + ZStack { | |
| 33 | + RoundedRectangle(cornerRadius: 14, style: .continuous) | |
| 34 | + .fill(KATheme.inkLight) | |
| 35 | + .frame(width: 56, height: 56) | |
| 36 | + Text("KA").font(.system(size: 22, weight: .bold, design: .rounded)) | |
| 37 | + .foregroundStyle(KATheme.lime) | |
| 38 | + .rotationEffect(.degrees(-4)) | |
| 39 | + } | |
| 40 | + VStack(alignment: .leading, spacing: 3) { | |
| 41 | + Text("Votre KA ID").font(.headline) | |
| 42 | + Text("Un seul compte pour les 13 plateformes de l'écosystème.") | |
| 43 | + .font(.caption).foregroundStyle(.secondary) | |
| 44 | + } | |
| 45 | + } | |
| 46 | + Button { | |
| 47 | + kaid.login() | |
| 48 | + } label: { | |
| 49 | + HStack { | |
| 50 | + Label("Se connecter avec KA ID", systemImage: "person.crop.circle.badge.checkmark") | |
| 51 | + Spacer() | |
| 52 | + if kaid.busy { ProgressView() } | |
| 53 | + } | |
| 54 | + } | |
| 55 | + .disabled(kaid.busy) | |
| 56 | + Link(destination: Ecosystem.signupURL) { | |
| 57 | + Label("Créer un compte sur groupe-ka.com", systemImage: "arrow.up.right.square") | |
| 58 | + } | |
| 59 | + if let e = kaid.lastError { | |
| 60 | + Text(e).font(.caption).foregroundStyle(.red) | |
| 32 | 61 | } |
| 33 | − } | |
| 34 | − Link(destination: Ecosystem.signupURL) { | |
| 35 | − Label("Se connecter / créer un compte KA ID", systemImage: "person.crop.circle.badge.checkmark") | |
| 36 | 62 | } |
| 37 | 63 | } |
| 38 | 64 | |
modified
KA/Features/UniversesView.swift
+32 −0
@@ -195,6 +195,25 @@ struct ItemDetailView: View { | ||
| 195 | 195 | var body: some View { |
| 196 | 196 | ScrollView { |
| 197 | 197 | VStack(alignment: .leading, spacing: 16) { |
| 198 | + if let img = item.imageURL { | |
| 199 | + AsyncImage(url: img) { phase in | |
| 200 | + switch phase { | |
| 201 | + case .success(let image): | |
| 202 | + image.resizable().aspectRatio(contentMode: .fill) | |
| 203 | + case .failure: | |
| 204 | + EmptyView() | |
| 205 | + default: | |
| 206 | + (universe?.accent ?? .gray).opacity(0.1) | |
| 207 | + .overlay(ProgressView()) | |
| 208 | + } | |
| 209 | + } | |
| 210 | + .frame(maxWidth: .infinity) | |
| 211 | + .frame(height: 230) | |
| 212 | + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) | |
| 213 | + .overlay(RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| 214 | + .strokeBorder(.primary.opacity(0.3), lineWidth: 1.2)) | |
| 215 | + .accessibilityLabel("Photo : \(item.title)") | |
| 216 | + } | |
| 198 | 217 | if let u = universe { |
| 199 | 218 | HStack { KAChip(text: u.wordmark, accent: u.accent); Spacer() } |
| 200 | 219 | } |
@@ -208,6 +227,19 @@ struct ItemDetailView: View { | ||
| 208 | 227 | if let c = item.city { KAChip(text: c) } |
| 209 | 228 | } |
| 210 | 229 | |
| 230 | + if let detail = item.detail, !detail.isEmpty { | |
| 231 | + VStack(alignment: .leading, spacing: 6) { | |
| 232 | + Text("À propos").font(.headline) | |
| 233 | + Text(detail) | |
| 234 | + .font(.subheadline) | |
| 235 | + .foregroundStyle(KATheme.ink2(scheme)) | |
| 236 | + .lineLimit(12) | |
| 237 | + } | |
| 238 | + .padding(14) | |
| 239 | + .frame(maxWidth: .infinity, alignment: .leading) | |
| 240 | + .kaCard() | |
| 241 | + } | |
| 242 | + | |
| 211 | 243 | if !item.facts.isEmpty { |
| 212 | 244 | VStack(spacing: 0) { |
| 213 | 245 | ForEach(item.facts, id: \.self) { f in |
added
KA/Info.plist
+44 −0
@@ -0,0 +1,44 @@ | ||
| 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 | + <key>CFBundleDevelopmentRegion</key> | |
| 6 | + <string>$(DEVELOPMENT_LANGUAGE)</string> | |
| 7 | + <key>CFBundleExecutable</key> | |
| 8 | + <string>$(EXECUTABLE_NAME)</string> | |
| 9 | + <key>CFBundleIdentifier</key> | |
| 10 | + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | |
| 11 | + <key>CFBundleInfoDictionaryVersion</key> | |
| 12 | + <string>6.0</string> | |
| 13 | + <key>CFBundleName</key> | |
| 14 | + <string>$(PRODUCT_NAME)</string> | |
| 15 | + <key>CFBundlePackageType</key> | |
| 16 | + <string>APPL</string> | |
| 17 | + <key>CFBundleShortVersionString</key> | |
| 18 | + <string>1.0</string> | |
| 19 | + <key>CFBundleURLTypes</key> | |
| 20 | + <array> | |
| 21 | + <dict> | |
| 22 | + <key>CFBundleURLName</key> | |
| 23 | + <string>com.groupeka.ka.auth</string> | |
| 24 | + <key>CFBundleURLSchemes</key> | |
| 25 | + <array> | |
| 26 | + <string>ka-ios</string> | |
| 27 | + </array> | |
| 28 | + </dict> | |
| 29 | + </array> | |
| 30 | + <key>CFBundleVersion</key> | |
| 31 | + <string>1</string> | |
| 32 | + <key>ITSAppUsesNonExemptEncryption</key> | |
| 33 | + <false/> | |
| 34 | + <key>UILaunchScreen</key> | |
| 35 | + <dict/> | |
| 36 | + <key>UISupportedInterfaceOrientations</key> | |
| 37 | + <array> | |
| 38 | + <string>UIInterfaceOrientationPortrait</string> | |
| 39 | + <string>UIInterfaceOrientationPortraitUpsideDown</string> | |
| 40 | + <string>UIInterfaceOrientationLandscapeLeft</string> | |
| 41 | + <string>UIInterfaceOrientationLandscapeRight</string> | |
| 42 | + </array> | |
| 43 | +</dict> | |
| 44 | +</plist> | |
modified
KA/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png
+0 −0
Binary file not shown.
modified
KATests/KATests.swift
+1 −1
@@ -78,7 +78,7 @@ final class AdapterTests: XCTestCase { | ||
| 78 | 78 | let store = FavoritesStore() |
| 79 | 79 | let item = KAItem(id: "t:1", universeID: "lou-ka", title: "Test", |
| 80 | 80 | subtitle: nil, priceLabel: nil, city: nil, url: nil, |
| 81 | − imageURL: nil, latitude: nil, longitude: nil, facts: []) | |
| 81 | + imageURL: nil, latitude: nil, longitude: nil, detail: nil, facts: []) | |
| 82 | 82 | XCTAssertFalse(store.isFavorite(item)) |
| 83 | 83 | store.toggle(item) |
| 84 | 84 | XCTAssertTrue(store.isFavorite(item)) |
added
docs/icon.svg
+60 −0
@@ -0,0 +1,60 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"> | |
| 2 | + <!-- Icône KA — Groupe-KA · éditorial sharp (encre, lime, ombre décalée, 12 univers) --> | |
| 3 | + <defs> | |
| 4 | + <radialGradient id="glow" cx="32%" cy="26%" r="75%"> | |
| 5 | + <stop offset="0%" stop-color="#d9f26b" stop-opacity="0.20"/> | |
| 6 | + <stop offset="55%" stop-color="#d9f26b" stop-opacity="0.05"/> | |
| 7 | + <stop offset="100%" stop-color="#d9f26b" stop-opacity="0"/> | |
| 8 | + </radialGradient> | |
| 9 | + <filter id="grain"> | |
| 10 | + <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/> | |
| 11 | + <feColorMatrix type="saturate" values="0"/> | |
| 12 | + <feComponentTransfer><feFuncA type="linear" slope="0.05"/></feComponentTransfer> | |
| 13 | + <feComposite operator="over" in2="SourceGraphic"/> | |
| 14 | + </filter> | |
| 15 | + </defs> | |
| 16 | + | |
| 17 | + <!-- fond encre + halo + grain --> | |
| 18 | + <rect width="1024" height="1024" fill="#141814"/> | |
| 19 | + <rect width="1024" height="1024" fill="url(#glow)"/> | |
| 20 | + <rect width="1024" height="1024" filter="url(#grain)" opacity="0.6"/> | |
| 21 | + | |
| 22 | + <!-- cadre papier fin (signature bordure) --> | |
| 23 | + <rect x="52" y="52" width="920" height="920" rx="64" fill="none" | |
| 24 | + stroke="#f5f3ee" stroke-opacity="0.16" stroke-width="6"/> | |
| 25 | + | |
| 26 | + <!-- kicker mono --> | |
| 27 | + <text x="512" y="188" text-anchor="middle" | |
| 28 | + font-family="Menlo, ui-monospace, monospace" font-size="44" font-weight="700" | |
| 29 | + letter-spacing="18" fill="#8b928c">·KA = AGRÉGER</text> | |
| 30 | + | |
| 31 | + <!-- KA : ombre décalée dure (vert profond) puis lime, légère rotation --> | |
| 32 | + <g transform="rotate(-4 512 560)"> | |
| 33 | + <text x="534" y="712" text-anchor="middle" | |
| 34 | + font-family="-apple-system, 'SF Pro Rounded', 'Arial Rounded MT Bold', sans-serif" | |
| 35 | + font-size="470" font-weight="800" letter-spacing="-18" | |
| 36 | + fill="#123f2e">KA</text> | |
| 37 | + <text x="512" y="690" text-anchor="middle" | |
| 38 | + font-family="-apple-system, 'SF Pro Rounded', 'Arial Rounded MT Bold', sans-serif" | |
| 39 | + font-size="470" font-weight="800" letter-spacing="-18" | |
| 40 | + fill="#d9f26b">KA</text> | |
| 41 | + </g> | |
| 42 | + | |
| 43 | + <!-- les 12 univers : pastilles d'accent officielles --> | |
| 44 | + <g transform="translate(512 862)"> | |
| 45 | + <g transform="translate(-341 0)"> | |
| 46 | + <circle cx="0" r="26" fill="#1c7ed6"/> | |
| 47 | + <circle cx="62" r="26" fill="#ff6a00"/> | |
| 48 | + <circle cx="124" r="26" fill="#e23744"/> | |
| 49 | + <circle cx="186" r="26" fill="#ff5148"/> | |
| 50 | + <circle cx="248" r="26" fill="#ff5a2a"/> | |
| 51 | + <circle cx="310" r="26" fill="#c4532e"/> | |
| 52 | + <circle cx="372" r="26" fill="#1f9d55"/> | |
| 53 | + <circle cx="434" r="26" fill="#f08c00"/> | |
| 54 | + <circle cx="496" r="26" fill="#d6336c"/> | |
| 55 | + <circle cx="558" r="26" fill="#7048e8"/> | |
| 56 | + <circle cx="620" r="26" fill="#3b5bdb"/> | |
| 57 | + <circle cx="682" r="26" fill="#0c8599"/> | |
| 58 | + </g> | |
| 59 | + </g> | |
| 60 | +</svg> | |
added
docs/screenshots/04-accueil-vignettes.png
+0 −0
Binary file not shown.
modified
project.yml
+16 −4
@@ -10,7 +10,7 @@ settings: | ||
| 10 | 10 | base: |
| 11 | 11 | SWIFT_VERSION: "5.0" |
| 12 | 12 | MARKETING_VERSION: "1.0.0" |
| 13 | − CURRENT_PROJECT_VERSION: "1" | |
| 13 | + CURRENT_PROJECT_VERSION: "3" | |
| 14 | 14 | DEVELOPMENT_TEAM: "3YM54G49SN" |
| 15 | 15 | CODE_SIGN_STYLE: Automatic |
| 16 | 16 | GENERATE_INFOPLIST_FILE: true |
@@ -27,13 +27,25 @@ targets: | ||
| 27 | 27 | settings: |
| 28 | 28 | base: |
| 29 | 29 | PRODUCT_BUNDLE_IDENTIFIER: com.groupeka.ka |
| 30 | + TARGETED_DEVICE_FAMILY: "1" | |
| 31 | + INFOPLIST_KEY_CFBundleDevelopmentRegion: fr | |
| 30 | 32 | INFOPLIST_KEY_CFBundleDisplayName: KA |
| 31 | 33 | INFOPLIST_KEY_UILaunchScreen_Generation: true |
| 32 | 34 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" |
| 33 | 35 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription: "KA affiche les logements, restos, sorties et emplois autour de vous sur la carte unifiée." |
| 34 | − INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: false | |
| 35 | − INFOPLIST_KEY_CFBundleDevelopmentRegion: fr | |
| 36 | − TARGETED_DEVICE_FAMILY: "1" | |
| 36 | + info: | |
| 37 | + path: KA/Info.plist | |
| 38 | + properties: | |
| 39 | + CFBundleURLTypes: | |
| 40 | + - CFBundleURLName: com.groupeka.ka.auth | |
| 41 | + CFBundleURLSchemes: [ka-ios] | |
| 42 | + UISupportedInterfaceOrientations: | |
| 43 | + - UIInterfaceOrientationPortrait | |
| 44 | + - UIInterfaceOrientationPortraitUpsideDown | |
| 45 | + - UIInterfaceOrientationLandscapeLeft | |
| 46 | + - UIInterfaceOrientationLandscapeRight | |
| 47 | + UILaunchScreen: {} | |
| 48 | + ITSAppUsesNonExemptEncryption: false | |
| 37 | 49 | KAWidgets: |
| 38 | 50 | type: app-extension |
| 39 | 51 | platform: iOS |
| 40 | 52 | |