Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .claude/plans/2026-08-20-ui-test-tab-bar-rewrite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# UI test rewrite for the tab-bar UI

Shipping the tab-bar UI to everyone (`BetaFlags.Option.newUI` → `.shipped`) removed the
v1 scanner chrome that the XCUITest suite navigated through. The affected tests are
skipped via `BaseUITestCase.skipPendingTabBarRewrite(_:)` so the release can run; this
is the map for putting them back.

Grep `skipPendingTabBarRewrite` for the live list. The helper is deleted with the last
call site.

## What moved

| v1 affordance | v2 route |
|---|---|
| `ScanTopBar` Settings button (`app.buttons["Settings"]`) | You tab (`app.buttons["You"]`) → `YouScreen` settings list |
| `ScanBottomBar` Cash button (`app.buttons["Cash"]`) | no scanner give entry; per-currency **Give** on `CurrencyInfoScreen`, or the `flipcash://give` deeplink |
| `scan-wallet-button` | Wallet tab (`app.buttons["Wallet"]`) |
| `scan-tips-button` | Chat tab (`app.buttons["Chat"]`) — embedded, so no `navigationBars["Tips"]` Close button |
| `scan-discover-button` | Wallet tab → "Discover Currencies" tile (a push, not a sheet) |
| `discover-create-currency-card` promo | Wallet tab → "Create a Currency" tile — `CurrencyDiscoveryScreen.hidesPromo` hides the card |
| Settings "Add Money" / "Withdraw Money" rows | Wallet tab tiles of the same name |

## Gotchas

- **The tab bar hides on push.** `HomeTabView.isTabBarHidden` is true when a card is
expanded, a bill is showing, or the active tab's stack is non-empty. So
`assertMainScreenReached()` (now the Wallet tab) only holds at a tab root — pop first.
- **The You tab gates on a tip profile.** `YouScreen` (and therefore the whole settings
list) only renders when `session.profile?.isTippable == true`; otherwise the tab shows
`TipCardSetupPrompt`. Fresh-account tests cannot reach Settings this way.
- **`GiveDiscoverGateRegressionTests` tests behavior that no longer exists.** USDF is
giveable now (`BetaFlags.allowsDollarsGive`), so `GiveCashGate.discoverCurrencies` is
unreachable and the "No Community Currencies Yet" dialog never shows. Delete the test
with the phase-2 teardown rather than rewriting it.
- **`BaseUITestCase.navigateToGiveAmount()` still taps the v1 Cash button.** Its only
callers are skipped; rewrite it against whichever give entry the tests should cover.

## Product gap worth confirming separately

Settings is reachable *only* from the You tab, which requires a tippable profile. An
account without one appears to have no route to Settings at all.
12 changes: 10 additions & 2 deletions Flipcash/Core/Controllers/BetaFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ class BetaFlags {
}

/// Returns `true` when the given beta flag is currently active.
///
/// A `.shipped` option reports on for everyone regardless of what is stored,
/// so a rollout needs no call-site edits and no user can opt back out.
func hasEnabled(_ option: Option) -> Bool {
options.contains(option)
if option.availability == .shipped { return true }
return options.contains(option)
}

/// Whether Dollars (USDF) can be given, sent, or tipped like a community
Expand Down Expand Up @@ -156,7 +160,7 @@ extension BetaFlags {
switch self {
case .vibrateOnScan: return .developer
case .enableCoinbase: return .developer
case .newUI: return .developer
case .newUI: return .shipped
}
}
}
Expand All @@ -167,6 +171,10 @@ extension BetaFlags {
case developer
/// The public "Advanced ▸ Beta Features" screen, visible to every user.
case publicBeta
/// Shipped to everyone: forced on by ``BetaFlags/hasEnabled(_:)`` and
/// listed nowhere, since there is no longer a choice to offer. The case
/// stays until its branches are torn out of the call sites.
case shipped
}
}

Expand Down
8 changes: 6 additions & 2 deletions FlipcashUITests/Regression/AddMoneyGateRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import XCTest
/// no auth keys are required.
final class AddMoneyGateRegressionTests: BaseUITestCase {

func testBuyWithNoAssets_offersAddMoneyOnAmountEntry() {
func testBuyWithNoAssets_offersAddMoneyOnAmountEntry() throws {
try skipPendingTabBarRewrite("Discover moved from the scanner bottom bar to a Wallet tile")

let addMoney = AddMoneyStartScreen(app: app)
let currencyInfo = CurrencyInfoUIScreen(app: app)

Expand Down Expand Up @@ -39,7 +41,9 @@ final class AddMoneyGateRegressionTests: BaseUITestCase {
addMoney.assertSelectMethodReached()
}

func testCreateCurrencyWithNoAssets_gatesOnAddMoney() {
func testCreateCurrencyWithNoAssets_gatesOnAddMoney() throws {
try skipPendingTabBarRewrite("currency creation starts from a Wallet tile now, not the Discover promo card")

let addMoney = AddMoneyStartScreen(app: app)

createFreshAccount()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ final class ApplicationLogsRegressionTests: BaseUITestCase {

// MARK: - Tests

func testApplicationLogs_shareLogsPresentsShareSheet() {
func testApplicationLogs_shareLogsPresentsShareSheet() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)

assertMainScreenReached()
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Regression/CashLinkRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ final class CashLinkRegressionTests: BaseUITestCase {

override var requiresAuthentication: Bool { true }

func testCashLink_createAndCancel() {
func testCashLink_createAndCancel() throws {
try skipPendingTabBarRewrite("the cash link is created from the give flow, which no longer starts from a Cash button")

let wallet = WalletScreen(app: app)
let currencyInfo = CurrencyInfoUIScreen(app: app)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ final class GiveDiscoverGateRegressionTests: BaseUITestCase {

override var requiresUsdfOnlyAccount: Bool { true }

func testGiveWithUsdfOnly_routesToDiscover() {
func testGiveWithUsdfOnly_routesToDiscover() throws {
try skipPendingTabBarRewrite("USDF is giveable in the tab-bar UI, so the Discover nudge this asserts no longer exists")

assertMainScreenReached()

waitAndTap(app.buttons["Cash"])
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Regression/GiveRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import XCTest
/// required, runs the same on Xcode Cloud as locally.
final class GiveRegressionTests: BaseUITestCase {

func testGiveWithNoBalance_showsAddMoneyWithoutPresentingAmountEntry() {
func testGiveWithNoBalance_showsAddMoneyWithoutPresentingAmountEntry() throws {
try skipPendingTabBarRewrite("the no-balance gate is no longer reached by tapping Cash on the scanner")

// Walk the fastest fresh-account path: write-down branch, no Photos
// permission needed.
waitAndTap(app.buttons["Create a New Account"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ final class WithdrawPickerEmptyRegressionTests: BaseUITestCase {

override var requiresUsdfOnlyAccount: Bool { true }

func testWithdrawPicker_showsUsdfRow_onUsdfOnlyAccount() {
func testWithdrawPicker_showsUsdfRow_onUsdfOnlyAccount() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)

assertMainScreenReached()
Expand Down
12 changes: 9 additions & 3 deletions FlipcashUITests/Smoke/AccessKeyBackupSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ final class AccessKeyBackupSmokeTests: BaseUITestCase {

// MARK: - Tests

func testAccessKeyBackup_viewFromSettings() {
func testAccessKeyBackup_viewFromSettings() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)

assertMainScreenReached()
Expand All @@ -36,7 +38,9 @@ final class AccessKeyBackupSmokeTests: BaseUITestCase {
)
}

func testAccessKeyBackup_copyToClipboard() {
func testAccessKeyBackup_copyToClipboard() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)

assertMainScreenReached()
Expand Down Expand Up @@ -69,7 +73,9 @@ final class AccessKeyBackupSmokeTests: BaseUITestCase {
waitUntilHittableAndTap(copyButton, timeout: 5, "Expected 'Copy' option in context menu")
}

func testAccessKeyBackup_saveToPhotos() {
func testAccessKeyBackup_saveToPhotos() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)

assertMainScreenReached()
Expand Down
2 changes: 2 additions & 0 deletions FlipcashUITests/Smoke/BlockUnblockSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ final class BlockUnblockSmokeTests: BaseUITestCase {
/// Blocks the first tip-DM counterpart, asserts their chat leaves the Tips
/// list, then unblocks them from Settings and asserts the block is gone.
func testBlock_hidesTipConversation_thenUnblockRestores() throws {
try skipPendingTabBarRewrite("the Tips list is the Chat tab now — no sheet to open or close")

let tips = TipsUIScreen(app: app)
let settings = SettingsUIScreen(app: app)
let blocked = BlockedUsersUIScreen(app: app)
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Smoke/BlockedUsersSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ final class BlockedUsersSmokeTests: BaseUITestCase {

/// Main → Settings → My Account → Blocked lands on the Blocked list and it
/// loads its state (empty or populated) without hanging.
func testBlockedUsers_reachableFromMyAccount() {
func testBlockedUsers_reachableFromMyAccount() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let settings = SettingsUIScreen(app: app)
let blocked = BlockedUsersUIScreen(app: app)

Expand Down
8 changes: 6 additions & 2 deletions FlipcashUITests/Smoke/DepositSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ final class DepositSmokeTests: BaseUITestCase {

override var requiresAuthentication: Bool { true }

func testDeposit_landsOnUSDCEducationScreenWithBothButtons() {
func testDeposit_landsOnUSDCEducationScreenWithBothButtons() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let education = USDCDepositEducationScreen(app: app)

assertMainScreenReached()
Expand All @@ -20,7 +22,9 @@ final class DepositSmokeTests: BaseUITestCase {
XCTAssertTrue(education.depositOtherCurrenciesButton.exists)
}

func testDeposit_pickerKeepsUSDFAndRoutesToDirectAddress() {
func testDeposit_pickerKeepsUSDFAndRoutesToDirectAddress() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

let education = USDCDepositEducationScreen(app: app)

assertMainScreenReached()
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Smoke/DiscoverCurrenciesSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import XCTest
/// the Create-Your-Own-Currency promo card pushes the creation summary screen.
final class DiscoverCurrenciesSmokeTests: BaseUITestCase {

func testDiscover_newAccount_tapPromoCard_opensCurrencyCreation() {
func testDiscover_newAccount_tapPromoCard_opensCurrencyCreation() throws {
try skipPendingTabBarRewrite("Discover is a Wallet tile now, and the create-currency promo card is hidden")

// Create a brand new empty account so this test runs without a UITest access key.
waitAndTap(app.buttons["Create a New Account"])
waitAndTap(app.buttons["Wrote the 12 Words Down Instead?"])
Expand Down
2 changes: 1 addition & 1 deletion FlipcashUITests/Smoke/ForceLogoutSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class ForceLogoutSmokeTests: BaseUITestCase {

// Main screen must NOT be reachable
XCTAssertFalse(
app.buttons["Cash"].exists,
app.buttons["Wallet"].exists,
"Main screen must not be reachable with an unlocked access key"
)
}
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Smoke/GiveSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ final class GiveSmokeTests: BaseUITestCase {

override var requiresAuthentication: Bool { true }

func testGiveFlow_showsBillWithSendAsLink() {
func testGiveFlow_showsBillWithSendAsLink() throws {
try skipPendingTabBarRewrite("give no longer starts from a Cash button on the scanner")

assertMainScreenReached()

let amountEntry = navigateToGiveAmount()
Expand Down
2 changes: 2 additions & 0 deletions FlipcashUITests/Smoke/ProfileCreationSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ final class ProfileCreationSmokeTests: BaseUITestCase {
}

func testCreateProfile() throws {
try skipPendingTabBarRewrite("Tips is the Chat tab now, not a sheet off the scanner bottom bar")

createFreshAccount()

waitAndTap(app.buttons["scan-tips-button"])
Expand Down
4 changes: 3 additions & 1 deletion FlipcashUITests/Smoke/WithdrawSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ final class WithdrawSmokeTests: BaseUITestCase {
/// which lists every balance (Dollars included). USDF no longer gets a
/// dedicated intro-first entry; the "Withdraw as USDC" screen is reached by
/// picking Dollars.
func testWithdraw_landsOnCurrencyPicker() {
func testWithdraw_landsOnCurrencyPicker() throws {
try skipPendingTabBarRewrite("the settings list moved to the You tab; Add/Withdraw Money are Wallet tiles now")

assertMainScreenReached()

openWithdrawFromSettings()
Expand Down
20 changes: 18 additions & 2 deletions FlipcashUITests/Support/BaseUITestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,30 @@ class BaseUITestCase: XCTestCase {
element.tap()
}

/// Asserts that the main screen (ScanScreen) has been reached by checking for the Cash button.
/// Asserts that the main screen has been reached by checking for the Wallet
/// tab, the launch tab of the tab-bar UI.
///
/// The tab bar hides itself once a screen is pushed onto the active tab's
/// stack, so this only holds at a tab root — which is what "main screen"
/// means at every call site.
func assertMainScreenReached(timeout: TimeInterval = 30, _ message: String = "Expected to reach the main screen") {
XCTAssertTrue(
app.buttons["Cash"].waitForExistence(timeout: timeout),
app.buttons["Wallet"].waitForExistence(timeout: timeout),
message
)
}

/// Skips a test whose entry point was the v1 scanner chrome, removed when
/// the tab-bar UI shipped to everyone.
///
/// These flows still exist but are reached differently now, so each call
/// site needs a rewrite verified against a simulator rather than a selector
/// swap. Grep this symbol for the outstanding list; it goes away with the
/// last one.
func skipPendingTabBarRewrite(_ detail: String) throws {
throw XCTSkip("Pending rewrite for the tab-bar UI: \(detail)")
}

/// Navigates into the Give flow, retrying up to 3 times if the balance hasn't loaded yet.
/// On CI the balance may not be fetched immediately, showing a "No Balance Yet" dialog.
/// Returns an `AmountEntryScreen` ready for amount entry.
Expand Down