From 2db96519d7a130656849e88d7f4b4dd4ffd746d1 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 5 Oct 2026 19:23:45 -0700 Subject: [PATCH] Whistlegraph 108: ask for AI consent at first use --- apple/whistlegraph/README.md | 15 +++ .../Sources/NativeScreenFixture.swift | 6 +- .../Sources/WhistlegraphApp.swift | 106 ++++++++++++++++-- .../Sources/WhistlegraphPrivacy.swift | 60 +++++++++- .../Sources/WhistlegraphScreen.swift | 5 +- .../whistlegraph/UITests/AIConsentTests.swift | 70 ++++++++++++ .../Whistlegraph.xcodeproj/project.pbxproj | 10 +- apple/whistlegraph/project.yml | 2 +- 8 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 apple/whistlegraph/UITests/AIConsentTests.swift diff --git a/apple/whistlegraph/README.md b/apple/whistlegraph/README.md index e9fa6d623f..dc91407fdf 100644 --- a/apple/whistlegraph/README.md +++ b/apple/whistlegraph/README.md @@ -196,6 +196,21 @@ AC's IPFS gateway; its sandbox cannot access the mint page or wallet storage. ## App Store monetization +Build 108 asks for AI permission at the first Send, retry, or Talk action. Launch, +sign-in, typing and drawing do not open the prompt. The compact sheet identifies +the shared content and providers; Allow remembers the account's choice and +continues a pending typed/drawing request after the WebView gate is updated. +Not now or dismissing the sheet preserves the draft. Talk requires a new hold +after permission, so dismissing a sheet never starts the microphone. Account or +piece changes discard a pending continuation. Existing permissions are retained. +Brain → AI & privacy keeps the detailed controls, including the separate, +optional cloud speech and narration permissions, which remain off by default. + +`WhistlegraphUITests/AIConsentTests` exercises first use, cancellation, continuation, +relaunch and Talk against the `consent` fixture on a simulator or paired phone. +That fixture has a separate consent identity and source; it never reads the +real sign-in token, recovers purchases, starts the microphone or calls inference. + The first global release uses StoreKit consumables for hosted AC inference. `computer.aesthetic.walkieware.braincells.1m` grants 1,000,000 nonexpiring braincells to the signed-in AC account. Its button uses Apple's localized diff --git a/apple/whistlegraph/Sources/NativeScreenFixture.swift b/apple/whistlegraph/Sources/NativeScreenFixture.swift index 59b517ee9a..9b653a4915 100644 --- a/apple/whistlegraph/Sources/NativeScreenFixture.swift +++ b/apple/whistlegraph/Sources/NativeScreenFixture.swift @@ -1,15 +1,15 @@ #if DEBUG import Foundation -// Isolated screen fixtures; only the story export fixture runs on a phone. +// Isolated screen fixtures; story export and AI permission checks also run on a phone. // Never connect a fixture to a cloud thread. enum NativeScreenFixture { static var mode: String { ProcessInfo.processInfo.environment["WALKIE_NATIVE_SCREEN_FIXTURE"] ?? "" } static var enabled: Bool { #if targetEnvironment(simulator) - return ["history", "recording", "gestures", "working", "story", "audio"].contains(mode) + return ["history", "recording", "gestures", "working", "story", "audio", "consent"].contains(mode) #else - return mode == "story" + return mode == "story" || mode == "consent" #endif } static var script: String { diff --git a/apple/whistlegraph/Sources/WhistlegraphApp.swift b/apple/whistlegraph/Sources/WhistlegraphApp.swift index b66e166262..c3a0b71141 100644 --- a/apple/whistlegraph/Sources/WhistlegraphApp.swift +++ b/apple/whistlegraph/Sources/WhistlegraphApp.swift @@ -23,6 +23,10 @@ struct WhistlegraphApp: App { if NativeScreenFixture.mode == "audio" { Text(voice.audioTestResult).accessibilityIdentifier("audio-autoplay-result") } + if voice.isConsentFixture { + Text("Requests: \(voice.consentFixtureRequests)") + .accessibilityIdentifier("consent-fixture-requests") + } #endif if !voice.workspaceReady { VStack(spacing: 20) { @@ -34,14 +38,20 @@ struct WhistlegraphApp: App { }.padding(30).foregroundStyle(.primary) } } - .task { await voice.braincells.start(session: voice) } - .onChange(of: voice.snapshot.handle) { _, _ in Task { await voice.syncAIAccount(); await voice.braincells.accountChanged() } } - .sheet(isPresented: $voice.showingAIConsent) { NavigationStack { WhistlegraphPrivacySheet(session: voice) } } + .task { if !voice.isConsentFixture { await voice.braincells.start(session: voice) } } + .onChange(of: voice.snapshot.handle) { _, _ in Task { + await voice.syncAIAccount() + if !voice.isConsentFixture { await voice.braincells.accountChanged() } + } } + .sheet(isPresented: $voice.showingAIConsent, onDismiss: voice.finishAIConsentPrompt) { + WhistlegraphAIConsentSheet(canAllow: voice.canAllowAIConsent, + allow: voice.allowAIConsent, decline: voice.declineAIConsent) + } .preferredColorScheme(appearance == "light" ? .light : appearance == "dark" ? .dark : nil) .onChange(of: phase) { _, value in if value == .background { voice.cancelHold() } if value == .active && voice.capturePhase == .idle { voice.resumePieceAudio() } - if value == .active { + if value == .active && !voice.isConsentFixture { Task { await TezDisplayRate.shared.refresh() await voice.braincells.recover() @@ -203,6 +213,9 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand @Published var layout = NativeLayout() @Published var snapshot = PieceSnapshot() @Published var showingAIConsent = false + private var consentRequest: (subject: String, handle: String, generation: Int, code: String, head: Int)? + private var afterAIConsent: (() -> Void)? + private var acceptedAIConsent = false @Published private(set) var localDataRevision = 0 let aiConsent = AIConsent.shared @Published var engineReady = false @@ -218,7 +231,15 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand @Published var workspaceReady = false #if DEBUG @Published var audioTestResult = "Waiting for piece audio" + @Published var consentFixtureRequests = 0 #endif + var isConsentFixture: Bool { + #if DEBUG + return NativeScreenFixture.mode == "consent" + #else + return false + #endif + } @Published var pieces: [PieceSummary] = [] @Published private(set) var pixelSize = WhistlegraphPreview.savedPixelSize @Published private(set) var previewFormat = PreviewFormat.saved @@ -250,8 +271,9 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand func command(_ action: String, version: Int? = nil, text: String? = nil, piece: String? = nil) { guard ["checkout", "newPiece", "openPiece", "stop", "signIn", "ask", "retry", "presentVersion", "endPresentation", "setModel", "refreshBraincells"].contains(action) else { return } - if ["ask", "retry"].contains(action), !snapshot.handle.isEmpty, !aiConsent.creation { - showingAIConsent = true; return + if ["ask", "retry"].contains(action), !aiConsent.creation { + requestAIConsent { [weak self] in self?.command(action, version: version, text: text, piece: piece) } + return } if action == "newPiece" || action == "openPiece" { guard engineReady, !snapshot.busy, capturePhase == .idle else { return } @@ -279,11 +301,19 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand value["text"] = text if let sketch = drawing.payload() { value["drawing"] = sketch } } + #if DEBUG + if isConsentFixture && ["ask", "retry"].contains(action) { + consentFixtureRequests += 1; return // Exercise the native gate without paid inference. + } + #endif Task { _ = try? await webView?.callAsyncJavaScript("window.walkiewareNativeCommand?.(command)", arguments: ["command": value], in: nil, contentWorld: .page) } } func beginHold() { guard engineReady, !snapshot.busy, capturePhase == .idle else { return } - guard aiConsent.creation else { showingAIConsent = true; return } + guard !snapshot.handle.isEmpty else { command("signIn"); return } + // Permission does not start a microphone after the finger has lifted. + guard aiConsent.creation else { requestAIConsent(); return } + if isConsentFixture { return } performanceTurn = false captureError = nil; transcript = ""; speechStartedAt = nil; capturePhase = .opening webView?.evaluateJavaScript("voiceStart()") @@ -295,12 +325,62 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand } func endHold() { webView?.evaluateJavaScript("voiceEnd()") } func syncAIAccount() async { + if isConsentFixture { aiConsent.bind(subject: "fixture:ai-consent", handle: "preview"); return } let expected = account.generation let subject = try? await account.subject() guard expected == account.generation else { return } aiConsent.bind(subject: subject ?? nil, handle: snapshot.handle) } + var canAllowAIConsent: Bool { + guard let request = consentRequest else { return false } + return aiConsent.signedIn && request.subject == aiConsent.subject && + request.handle == snapshot.handle && request.generation == account.generation + } + func requestAIConsent(resume: (() -> Void)? = nil) { + guard !showingAIConsent else { return } + if aiConsent.creation { resume?(); return } + guard !snapshot.handle.isEmpty else { command("signIn"); return } + let generation = account.generation, handle = snapshot.handle + let code = snapshot.code, head = snapshot.head + Task { + await syncAIAccount() + guard generation == account.generation, handle == snapshot.handle, + code == snapshot.code, head == snapshot.head, !showingAIConsent, + let subject = aiConsent.subject, aiConsent.signedIn else { return } + if aiConsent.creation { resume?(); return } + consentRequest = (subject, handle, generation, code, head) + afterAIConsent = resume; acceptedAIConsent = false + showingAIConsent = true + } + } + func allowAIConsent() { + guard canAllowAIConsent else { declineAIConsent(); return } + aiConsent.set(\.creation, true) + acceptedAIConsent = true; showingAIConsent = false + } + func declineAIConsent() { + acceptedAIConsent = false; showingAIConsent = false + } + func finishAIConsentPrompt() { + let request = consentRequest, resume = afterAIConsent + let accepted = acceptedAIConsent && canAllowAIConsent + consentRequest = nil; afterAIConsent = nil; acceptedAIConsent = false + guard accepted, let request, let resume else { return } + Task { + // Await the WebView gate before continuing the original action. + guard let webView else { return } + do { + _ = try await webView.callAsyncJavaScript("window.__whistlegraphAIConsent = value; window.walkiewareSetAIConsent?.(value);", + arguments: ["value": aiConsent.bridge], in: nil, contentWorld: .page) + } catch { return } + guard aiConsent.creation, request.subject == aiConsent.subject, + request.handle == snapshot.handle, request.generation == account.generation, + request.code == snapshot.code, request.head == snapshot.head else { return } + resume() + } + } private func applyAIConsent() { + if showingAIConsent && !canAllowAIConsent { declineAIConsent() } if !aiConsent.creation { command("stop"); cancelHold() } if !aiConsent.cloudSpeech { cancelHold() } if !aiConsent.cloudNarration { StoryVoice.cancelCloudRequests() } @@ -398,6 +478,11 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand private let capture = SpeechCapture() override init() { super.init() + #if DEBUG + if isConsentFixture, ProcessInfo.processInfo.environment["WALKIE_RESET_AI_CONSENT"] == "1" { + UserDefaults.standard.removeObject(forKey: AIConsentRecord.key(subject: "fixture:ai-consent")) + } + #endif aiConsent.changed = { [weak self] in self?.applyAIConsent() } capture.hasVisualInput = { [weak self] in self?.drawing.hasInk == true } capture.speechToken = { [weak self] in @@ -574,10 +659,15 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand if AudioBenchmark.enabled || SequenceBenchmark.enabled { UIApplication.shared.isIdleTimerDisabled = true } Task { await NetworkBenchmark.run() } case "account": + if isConsentFixture { emitEngine(["kind": "account", "token": ""]); return } Task { do { emitEngine(["kind": "account", "token": try await account.token() ?? ""]) } catch { emitEngine(["kind": "account", "token": ""]) } } - case "aiConsent": showingAIConsent = true + case "aiConsent": + // The network guard can reject background recovery or stale account + // work. Only native Send/retry/Talk gestures may present permission. + break case "signIn": + if isConsentFixture { return } account.signIn(from: webView) { [weak self] result in switch result { case .success(let token): self?.emitEngine(["kind": "account", "token": token]) diff --git a/apple/whistlegraph/Sources/WhistlegraphPrivacy.swift b/apple/whistlegraph/Sources/WhistlegraphPrivacy.swift index 152f09d55c..cbfe26df02 100644 --- a/apple/whistlegraph/Sources/WhistlegraphPrivacy.swift +++ b/apple/whistlegraph/Sources/WhistlegraphPrivacy.swift @@ -4,7 +4,7 @@ import SwiftUI static let shared = AIConsent() @Published private(set) var record = AIConsentRecord() @Published private(set) var handle = "" - private var subject: String? + private(set) var subject: String? var changed: () -> Void = {} var signedIn: Bool { subject != nil && !handle.isEmpty } var creation: Bool { signedIn && record.creation } @@ -28,6 +28,64 @@ import SwiftUI } } +// This is presented by an attempted AI action, never by launch or sign-in. +// The separate settings page below remains available for changing permission. +struct WhistlegraphAIConsentSheet: View { + let canAllow: Bool + let allow: () -> Void + let decline: () -> Void + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dynamicTypeSize) private var typeSize + private var theme: WhistlegraphTheme { WhistlegraphTheme(phase: .ready, dark: colorScheme == .dark) } + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + ComicTitle(text: "Create with AI?", size: 30) + .accessibilityAddTraits(.isHeader) + Text("To make and check your piece, AC shares your typed and spoken words, drawings, code and version history, sound measurements and artwork previews with OpenRouter and your chosen AI provider.") + .font(.custom("ComicRelief-Regular", size: 18, relativeTo: .body)) + Text("Providers: Anthropic, OpenAI, DeepSeek, Moonshot AI, Alibaba (Qwen), MiniMax and Z.ai. Personal models use Anthropic or OpenAI directly.") + .font(.custom("ComicRelief-Regular", size: 15, relativeTo: .subheadline)) + Text("Change your choice in Brain → AI & privacy.") + .font(.custom("ComicRelief-Regular", size: 15, relativeTo: .subheadline)) + HStack(spacing: 12) { + Button("Not now", action: decline) + .accessibilityIdentifier("ai-consent-not-now") + .buttonStyle(ConsentButtonStyle(theme: theme, fill: theme.surface)) + Button("Allow", action: allow) + .accessibilityIdentifier("ai-consent-allow") + .buttonStyle(ConsentButtonStyle(theme: theme, fill: Color(red: 0.40, green: 0.83, blue: 0.95), prominent: true)) + .disabled(!canAllow) + } + Link("Privacy policy", destination: URL(string: "https://aesthetic.computer/privacy-policy.html")!) + .font(.custom("ComicRelief-Regular", size: 15, relativeTo: .subheadline)) + .frame(maxWidth: .infinity).padding(.top, 2) + }.padding(24).padding(.top, 12) + } + .foregroundStyle(theme.foreground).tint(theme.foreground) + .background(theme.background.ignoresSafeArea()) + .presentationBackground(theme.background) + .presentationDetents(typeSize.isAccessibilitySize ? [.large] : [.height(510), .large]) + .presentationDragIndicator(.visible) + .presentationCornerRadius(30) + } +} + +private struct ConsentButtonStyle: ButtonStyle { + let theme: WhistlegraphTheme + let fill: Color + var prominent = false + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.custom("ComicRelief-Bold", size: 22, relativeTo: .title3)) + .frame(maxWidth: .infinity, minHeight: 56) + .foregroundStyle(prominent ? theme.buttonInk : theme.foreground) + .background(fill, in: RoundedRectangle(cornerRadius: 22)) + .overlay(RoundedRectangle(cornerRadius: 22).strokeBorder(theme.foreground, lineWidth: 2)) + .opacity(configuration.isPressed ? 0.7 : 1) + } +} + struct WhistlegraphPrivacySheet: View { @ObservedObject var session: WhistlegraphSession @ObservedObject private var consent = AIConsent.shared diff --git a/apple/whistlegraph/Sources/WhistlegraphScreen.swift b/apple/whistlegraph/Sources/WhistlegraphScreen.swift index dd70ea41f3..a79ff28151 100644 --- a/apple/whistlegraph/Sources/WhistlegraphScreen.swift +++ b/apple/whistlegraph/Sources/WhistlegraphScreen.swift @@ -213,8 +213,9 @@ struct WhistlegraphScreen: View { Group { if narrator.isPlaying { EmptyView() } else if showComposer { InlineRequestComposer(theme: theme, disabled: session.snapshot.busy, hasDrawing: drawing.hasInk, cancel: { ButtonSounds.play(.pop); showComposer = false }) { text in - guard session.aiConsent.creation || session.snapshot.handle.isEmpty else { session.showingAIConsent = true; return } - ButtonSounds.play(.sent); session.command("ask", text: text); showComposer = false + session.requestAIConsent { + ButtonSounds.play(.sent); session.command("ask", text: text); showComposer = false + } } } else { HStack(spacing: 12 * (1 - chalkReveal)) { diff --git a/apple/whistlegraph/UITests/AIConsentTests.swift b/apple/whistlegraph/UITests/AIConsentTests.swift new file mode 100644 index 0000000000..7543210804 --- /dev/null +++ b/apple/whistlegraph/UITests/AIConsentTests.swift @@ -0,0 +1,70 @@ +import XCTest + +// Uses a separate fixture identity, source and consent record. No real account, +// recording or model request is used. The production native consent flow runs. +final class AIConsentTests: XCTestCase { + override func setUp() { continueAfterFailure = false } + private func launch() -> XCUIApplication { + let app = XCUIApplication() + app.launchEnvironment["WALKIE_NATIVE_SCREEN_FIXTURE"] = "consent" + app.launchEnvironment["WALKIE_RESET_AI_CONSENT"] = "1" + app.launch() + let account = app.buttons.matching(identifier: "workspace-account") + .matching(NSPredicate(format: "label == %@", "@preview, account")).firstMatch + XCTAssertTrue(account.waitForExistence(timeout: 40)) + XCTAssertFalse(app.buttons["ai-consent-allow"].exists, "No prompt at launch or sign-in") + return app + } + private func type(_ words: String, in app: XCUIApplication) { + XCTAssertEqual(app.state, .runningForeground, "The phone must be left in the test app") + app.buttons["type-control"].tap() + let field = app.textFields["typed-request"] + XCTAssertTrue(field.waitForExistence(timeout: 10)) + field.tap(); field.typeText(words) + XCTAssertFalse(app.buttons["ai-consent-allow"].exists, "Typing stays local") + app.buttons["Send"].tap() + } + private func count(_ number: Int, in app: XCUIApplication) { + let reached = expectation(for: NSPredicate(format: "label == %@", "Requests: \(number)"), + evaluatedWith: app.staticTexts["consent-fixture-requests"]) + XCTAssertEqual(XCTWaiter.wait(for: [reached], timeout: 10), .completed) + } + func testFirstSendDeclineAllowAndRelaunch() { + let app = launch() + type("Make a dancing tree", in: app) + let allow = app.buttons["ai-consent-allow"] + XCTAssertTrue(allow.waitForExistence(timeout: 10)) + XCTAssertTrue(allow.isHittable, "Primary action fits without scrolling") + XCTAssertTrue(app.buttons["ai-consent-not-now"].isHittable) + let image = XCTAttachment(screenshot: app.screenshot()) + image.name = "First AI use"; image.lifetime = .keepAlways; add(image) + app.buttons["ai-consent-not-now"].tap() + let field = app.textFields["typed-request"] + XCTAssertTrue(field.waitForExistence(timeout: 10)) + XCTAssertEqual(field.value as? String, "Make a dancing tree") + count(0, in: app) + app.buttons["Send"].tap() + XCTAssertTrue(allow.waitForExistence(timeout: 10)); allow.tap() + count(1, in: app) + XCTAssertFalse(field.exists, "Allow continues the original typed request") + type("Make it purple", in: app) + count(2, in: app) + XCTAssertFalse(allow.exists, "No repeated prompt") + app.terminate() + app.launchEnvironment["WALKIE_RESET_AI_CONSENT"] = "0" + app.launch() + XCTAssertTrue(app.buttons["type-control"].waitForExistence(timeout: 40)) + type("Make it spin", in: app) + count(1, in: app) + XCTAssertFalse(allow.exists, "Permission survives relaunch") + } + func testFirstTalkDoesNotStartMicrophoneAfterAllow() { + let app = launch() + app.buttons["talk-control"].press(forDuration: 0.2) + let allow = app.buttons["ai-consent-allow"] + XCTAssertTrue(allow.waitForExistence(timeout: 10)); allow.tap() + XCTAssertTrue(app.buttons["type-control"].waitForExistence(timeout: 10)) + XCTAssertFalse(app.staticTexts["Listening…"].exists) + count(0, in: app) + } +} diff --git a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj index e7ad4e71f0..82c34ff51a 100644 --- a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj +++ b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 2D8D89A7A8C4657353319EBE /* WhistlegraphSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB3C6140E5D943C3363C353 /* WhistlegraphSource.swift */; }; 2F01E350A7FF55359B002344 /* ButtonSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ABFC6C11146A1540B635EE6 /* ButtonSounds.swift */; }; 2F15D5A580A90A18E92D90F1 /* AudioBenchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC14C186FE1CB5B19AD1D72 /* AudioBenchmark.swift */; }; + 3459C827F644F26BCABD5534 /* AIConsentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1D789A7001E472597133B /* AIConsentTests.swift */; }; 3CF6178F7BA1702ADFA457B7 /* StoryControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C42750EF4F7E96146B03585 /* StoryControls.swift */; }; 41FC24FA4CF0D6C4D0C9E7C3 /* WhistlegraphTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25F85E540016CEBC3A6BC09 /* WhistlegraphTheme.swift */; }; 45228E0CFD91217E60751356 /* WhistlegraphMintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C05FBB23D515547035D490 /* WhistlegraphMintTests.swift */; }; @@ -116,6 +117,7 @@ 65C2FC346829168FF4AEEED3 /* StoryPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPreview.swift; sourceTree = ""; }; 6D137824491A01D4C19417B8 /* VersionNarrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNarrator.swift; sourceTree = ""; }; 6D7D7F240AB7ADAA1340FE5C /* StoreCreditDelivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreditDelivery.swift; sourceTree = ""; }; + 6FB1D789A7001E472597133B /* AIConsentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIConsentTests.swift; sourceTree = ""; }; 725C2A9ACD0323F49EFFDFC1 /* AccountDeletionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionClient.swift; sourceTree = ""; }; 7848DB9FED6B955BC193F650 /* LiveTranscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTranscription.swift; sourceTree = ""; }; 85CE7F71D92E8328A7BDA44D /* PreviewFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewFormatTests.swift; sourceTree = ""; }; @@ -233,6 +235,7 @@ A8D1DCE6550AE55C396F659B /* UITests */ = { isa = PBXGroup; children = ( + 6FB1D789A7001E472597133B /* AIConsentTests.swift */, 3F98F7D3CE551FC6458D1F37 /* HeaderSheetsTests.swift */, 85CE7F71D92E8328A7BDA44D /* PreviewFormatTests.swift */, B8B4655A00B6DC810F7203D1 /* StoryCardsTests.swift */, @@ -441,6 +444,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3459C827F644F26BCABD5534 /* AIConsentTests.swift in Sources */, 19717448D7598ADAABBF739E /* HeaderSheetsTests.swift in Sources */, 67D674B7AF9E1E056C10BEDF /* PreviewFormatTests.swift in Sources */, 87EDDAE34B992D308236F1A9 /* StoryCardsTests.swift in Sources */, @@ -582,7 +586,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 107; + CURRENT_PROJECT_VERSION = 108; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -667,7 +671,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 107; + CURRENT_PROJECT_VERSION = 108; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_NS_ASSERTIONS = NO; @@ -727,7 +731,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 107; + CURRENT_PROJECT_VERSION = 108; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/apple/whistlegraph/project.yml b/apple/whistlegraph/project.yml index a88f888ec8..a89e153446 100644 --- a/apple/whistlegraph/project.yml +++ b/apple/whistlegraph/project.yml @@ -16,7 +16,7 @@ settings: CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.0" MARKETING_VERSION: "0.1.0" - CURRENT_PROJECT_VERSION: "107" + CURRENT_PROJECT_VERSION: "108" targets: Whistlegraph: type: application -- 2.51.2