From 33634bd8aa34beb896ccff066d6f4faaee6f3ace Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 5 Oct 2026 12:50:30 -0700 Subject: [PATCH] Verify Temple handoff on iPhone and fix checkout URL validation --- apple/whistlegraph/README.md | 3 ++ .../whistlegraph/Sources/BrainSettings.swift | 11 ++++-- .../Sources/TezosBraincells.swift | 2 +- .../Sources/WhistlegraphApp.swift | 2 +- .../UITests/HeaderSheetsTests.swift | 38 +++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apple/whistlegraph/README.md b/apple/whistlegraph/README.md index 14a415db9f..39a12c587d 100644 --- a/apple/whistlegraph/README.md +++ b/apple/whistlegraph/README.md @@ -144,3 +144,6 @@ testing. The API flag `AC_TEZOS_CREDITS_ENABLED` controls new purchases. `HeaderSheetsTests/testPhoneCostUnitToggle` checks all three units and persistence on the paired phone without initiating a purchase. +`HeaderSheetsTests/testPhoneLiveTezosCheckout` checks the live rate, authenticated +checkout in the default browser, and handoff to the installed Temple app. It +stops before wallet connection approval, signing, or sending tez. diff --git a/apple/whistlegraph/Sources/BrainSettings.swift b/apple/whistlegraph/Sources/BrainSettings.swift index 34f683e6c2..d24022f939 100644 --- a/apple/whistlegraph/Sources/BrainSettings.swift +++ b/apple/whistlegraph/Sources/BrainSettings.swift @@ -91,7 +91,7 @@ struct BrainSettings: View { } else { Text(inference.braincellsError).foregroundStyle(.secondary) } - Button("Refresh") { session.command("refreshBraincells") } + Button("Refresh") { session.command("refreshBraincells"); Task { await prices.refresh() } } TezosPurchaseButton(session: session, purchase: session.tezosBraincells) } if let usage = inference.usage { @@ -150,12 +150,17 @@ struct ThreadCostLabel: View { @AppStorage(CostUnit.preference) private var unit: CostUnit = .usd @ObservedObject private var prices = TezDisplayRate.shared private var amount: String { unit.amount(usd: cost.usd, rate: prices.rate, partial: cost.partial) } + private var value: String { + if cost.partial && cost.usd == 0 { return "Cost unavailable" } + if unit == .tezos && prices.rate?.isFresh != true { return "Rate unavailable" } + return (cost.partial ? "≥ " : "") + (cost.estimated == true || unit == .tezos ? "≈ " : "") + amount + } var body: some View { - Text((cost.partial ? "≥ " : "") + (cost.estimated == true || unit == .tezos ? "≈ " : "") + amount) + Text(value) .font(.caption.monospacedDigit()).foregroundStyle(.secondary) .lineLimit(1).fixedSize(horizontal: true, vertical: false) .accessibilityLabel("Provider inference value, " + unit.label) - .accessibilityValue((cost.partial ? "At least " : "") + amount) + .accessibilityValue(value) .accessibilityIdentifier("thread-cost") .task(id: unit) { if unit == .tezos { await prices.refresh() } } } diff --git a/apple/whistlegraph/Sources/TezosBraincells.swift b/apple/whistlegraph/Sources/TezosBraincells.swift index 11d483e8b5..4b9813386c 100644 --- a/apple/whistlegraph/Sources/TezosBraincells.swift +++ b/apple/whistlegraph/Sources/TezosBraincells.swift @@ -52,7 +52,7 @@ import StoreKit if checkout == nil { let result = try await request("create", bearer: token) guard let url = result.checkoutURL, url.scheme == "https", url.host == "aesthetic.computer", - url.path == "/braincells/", let secret = url.fragment, secret.count == 64, + url.path == "/braincells", url.hasDirectoryPath, let secret = url.fragment, secret.count == 64, secret.allSatisfy({ $0.isHexDigit }) else { throw NativeSignIn.failure("Invalid checkout link.") } let saved = Pending(url: url, handle: session.snapshot.handle) UserDefaults.standard.set(try JSONEncoder().encode(saved), forKey: storageKey) diff --git a/apple/whistlegraph/Sources/WhistlegraphApp.swift b/apple/whistlegraph/Sources/WhistlegraphApp.swift index 0ee6cc16e2..f039461a15 100644 --- a/apple/whistlegraph/Sources/WhistlegraphApp.swift +++ b/apple/whistlegraph/Sources/WhistlegraphApp.swift @@ -38,7 +38,7 @@ struct WhistlegraphApp: App { .onChange(of: phase) { _, value in if value == .background { voice.cancelHold() } if value == .active && voice.capturePhase == .idle { voice.resumePieceAudio() } - if value == .active { Task { await voice.tezosBraincells.refresh(session: voice) } } + if value == .active { Task { await TezDisplayRate.shared.refresh(); await voice.tezosBraincells.refresh(session: voice) } } } .onOpenURL { url in if url.scheme == "whistlegraph" && url.host == "braincells" { diff --git a/apple/whistlegraph/UITests/HeaderSheetsTests.swift b/apple/whistlegraph/UITests/HeaderSheetsTests.swift index 72dcab8cde..11e19e4ec5 100644 --- a/apple/whistlegraph/UITests/HeaderSheetsTests.swift +++ b/apple/whistlegraph/UITests/HeaderSheetsTests.swift @@ -30,6 +30,44 @@ final class HeaderSheetsTests: XCTestCase { app.buttons["Done"].tap() } + // Opens Temple; never approves a wallet connection, signature or payment. + func testPhoneLiveTezosCheckout() { + let app = XCUIApplication() + app.launch() + let brain = app.buttons["brain-settings"] + XCTAssertTrue(brain.waitForExistence(timeout: 30)) + brain.tap(); app.swipeUp() + let units = app.segmentedControls["brain-cost-unit"] + XCTAssertTrue(units.waitForExistence(timeout: 15)) + units.buttons["Tezos"].tap() + let rate = app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "1 tez = ")).firstMatch + XCTAssertTrue(rate.waitForExistence(timeout: 20), "live timestamped Tezos rate") + let balance = XCTAttachment(screenshot: app.screenshot()) + balance.name = "Live Tezos balance"; balance.lifetime = .keepAlways; add(balance) + app.buttons["brain-buy-tezos"].tap() + let browsers = ["com.apple.mobilesafari", "com.google.chrome.ios"].map { XCUIApplication(bundleIdentifier: $0) } + let opened = expectation(for: NSPredicate { _, _ in browsers.contains { $0.state == .runningForeground } }, evaluatedWith: nil) + guard XCTWaiter.wait(for: [opened], timeout: 20) == .completed, + let browser = browsers.first(where: { $0.state == .runningForeground }) else { + XCTFail("default browser did not open"); return + } + let connect = browser.buttons["Connect wallet"] + guard connect.waitForExistence(timeout: 30) else { + let failure = XCTAttachment(screenshot: app.screenshot()); failure.name = "Checkout did not open"; failure.lifetime = .keepAlways; add(failure) + XCTFail("authenticated checkout did not open in the default browser"); return + } + connect.tap() + let temple = browser.staticTexts["Temple"] + XCTAssertTrue(temple.waitForExistence(timeout: 30), "Temple is offered on iOS") + let wallet = XCTAttachment(screenshot: browser.screenshot()) + wallet.name = "Temple wallet picker on iPhone"; wallet.lifetime = .keepAlways; add(wallet) + temple.tap() + let open = browser.alerts.buttons["Open"] + if open.waitForExistence(timeout: 3) { open.tap() } + let walletApp = XCUIApplication(bundleIdentifier: "com.madfish.temple-wallet") + XCTAssertTrue(walletApp.wait(for: .runningForeground, timeout: 25), "checkout hands off to installed Temple") + } + func testPieceAudioStartsWithoutTouch() { let app = XCUIApplication() app.launchEnvironment["WALKIE_NATIVE_SCREEN_FIXTURE"] = "audio" -- 2.51.2