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
2,476 changes: 1,940 additions & 536 deletions Driveline/AppLifecycle/Localizable.xcstrings

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Driveline/UI/Home/HomePresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ enum HomePresenter {
}

static var newDriveButtonTitle: String {
String(localized: "New Drive", comment: "Button on empty state to start a new drive")
String(localized: "Record Your First Drive", comment: "Button on empty state to start a new drive")
}

static var automationSetupTitle: String {
String(localized: "Set Up Automated Recording", comment: "Title of the automation setup panel on the home screen")
}

static var automationSetupSubtitle: String {
String(localized: "Record drives hands-free when CarPlay connects.", comment: "Subtitle of the automation setup panel on the home screen")
String(localized: "Record drives hands-free when Bluetooth or CarPlay connects.", comment: "Subtitle of the automation setup panel on the home screen")
}
}
4 changes: 4 additions & 0 deletions Driveline/UI/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,14 @@ struct HomeView: View {
.onChange(of: driveService.isRecording, initial: true) { _, isRecording in
if isRecording { exitSelectMode() }
StatsPanelTip.isRecording = isRecording
RecordButtonTip.isRecording = isRecording
}
.onChange(of: recentStats.driveCount, initial: true) { _, count in
StatsPanelTip.driveCount = count
}
.onChange(of: drives.isEmpty, initial: true) { _, isEmpty in
RecordButtonTip.hasDrives = !isEmpty
}
}
.onContinueUserActivity(CSSearchableItemActionType) { activity in
guard let identifier = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { return }
Expand Down
28 changes: 19 additions & 9 deletions Driveline/UI/Onboarding/OnboardingAutomationDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,25 @@ struct OnboardingAutomationDetailView: View {
}

private var steps: [String] {
[
OnboardingPresenter.automationStartStep1,
OnboardingPresenter.automationStartStep2,
OnboardingPresenter.automationStartStep3,
OnboardingPresenter.automationStartStep4,
OnboardingPresenter.automationStartStep5,
OnboardingPresenter.automationStartStep6,
OnboardingPresenter.automationStartStep7
]
isStart
? [
OnboardingPresenter.automationStartStep1,
OnboardingPresenter.automationStartStep2,
OnboardingPresenter.automationStartStep3,
OnboardingPresenter.automationStartStep4,
OnboardingPresenter.automationStartStep5,
OnboardingPresenter.automationStartStep6,
OnboardingPresenter.automationStartStep7
]
: [
OnboardingPresenter.automationFinishStep1,
OnboardingPresenter.automationFinishStep2,
OnboardingPresenter.automationFinishStep3,
OnboardingPresenter.automationFinishStep4,
OnboardingPresenter.automationFinishStep5,
OnboardingPresenter.automationFinishStep6,
OnboardingPresenter.automationFinishStep7
]
}

private var primaryLabel: String {
Expand Down
14 changes: 7 additions & 7 deletions Driveline/UI/Onboarding/OnboardingPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,19 @@ enum OnboardingPresenter {
}

static var automationsIntroBody: String {
String(localized: "For truly hands-free recording, set up two automations in the Shortcuts app. They start and stop Driveline the moment you connect or disconnect from CarPlay.", comment: "Onboarding automations intro screen body")
String(localized: "For truly hands-free recording, set up two automations in the Shortcuts app. They start and stop Driveline the moment your iPhone connects to your car — over CarPlay or Bluetooth.", comment: "Onboarding automations intro screen body")
}

static var automationsIntroRow1Title: String {
String(localized: "CarPlay connects", comment: "Onboarding automations intro row 1 title")
String(localized: "When your car connects", comment: "Onboarding automations intro row 1 title")
}

static var automationsIntroRow1Body: String {
String(localized: "Driveline starts recording your drive.", comment: "Onboarding automations intro row 1 body")
}

static var automationsIntroRow2Title: String {
String(localized: "CarPlay disconnects", comment: "Onboarding automations intro row 2 title")
String(localized: "When your car disconnects", comment: "Onboarding automations intro row 2 title")
}

static var automationsIntroRow2Body: String {
Expand All @@ -136,7 +136,7 @@ enum OnboardingPresenter {
}

static var automationStartBody: String {
String(localized: "This automation starts recording the moment CarPlay connects — no button needed.", comment: "Onboarding automation start screen body")
String(localized: "This automation starts recording the moment CarPlay connects — or your car's Bluetooth pairs.", comment: "Onboarding automation start screen body")
}

static var automationStartStep1: String {
Expand All @@ -152,7 +152,7 @@ enum OnboardingPresenter {
}

static var automationStartStep4: String {
String(localized: "Choose **CarPlay** → **Connects**", comment: "Onboarding automation start step 4")
String(localized: "Choose **CarPlay** → **Connects** (or **Bluetooth** → your car)", comment: "Onboarding automation start step 4")
}

static var automationStartStep5: String {
Expand All @@ -174,7 +174,7 @@ enum OnboardingPresenter {
}

static var automationFinishBody: String {
String(localized: "This automation stops and saves your drive the moment CarPlay disconnects.", comment: "Onboarding automation finish screen body")
String(localized: "This automation stops and saves your drive the moment CarPlay disconnects — or your car's Bluetooth drops.", comment: "Onboarding automation finish screen body")
}

static var automationFinishStep1: String {
Expand All @@ -190,7 +190,7 @@ enum OnboardingPresenter {
}

static var automationFinishStep4: String {
String(localized: "Choose **CarPlay** → **Disconnects**", comment: "Onboarding automation finish step 4")
String(localized: "Choose **CarPlay** → **Disconnects** (or **Bluetooth** → your car)", comment: "Onboarding automation finish step 4")
}

static var automationFinishStep5: String {
Expand Down
4 changes: 4 additions & 0 deletions Driveline/UI/Tips/RecordButtonTip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import TipKit

struct RecordButtonTip: Tip {
@Parameter static var isOnboardingPresented: Bool = true
@Parameter static var hasDrives: Bool = false
@Parameter static var isRecording: Bool = false

var rules: [Rule] {
#Rule(Self.$isOnboardingPresented) { $0 == false }
#Rule(Self.$hasDrives) { $0 == true }
#Rule(Self.$isRecording) { $0 == false }
}

var title: Text {
Expand Down
7 changes: 7 additions & 0 deletions DrivelineTests/UITests/EditDriveTipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ struct EditDriveTipTests {
_ = EditDriveTip()
}

// MARK: - Rules

@Test
func gatesOnOnboardingDismissed() {
#expect(EditDriveTip().rules.count == 1)
}

// MARK: - isOnboardingPresented parameter

@Test
Expand Down
33 changes: 28 additions & 5 deletions DrivelineTests/UITests/RecordButtonTipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,47 @@ struct RecordButtonTipTests {

init() {
RecordButtonTip.isOnboardingPresented = true
RecordButtonTip.hasDrives = false
RecordButtonTip.isRecording = false
}

// MARK: - Instantiation

@Test
func tipCanBeInstantiated() {
_ = RecordButtonTip()
}

// MARK: - isOnboardingPresented parameter
// MARK: - Rules

@Test
func gatesOnOnboardingDismissedAndHavingDrivesAndNotRecording() {
#expect(RecordButtonTip().rules.count == 3)
}

// MARK: - Parameters

@Test
func hasDrivesDefaultsToFalse() {
#expect(RecordButtonTip.hasDrives == false)
}

@Test
func isOnboardingPresentedDefaultsToTrue() {
#expect(RecordButtonTip.isOnboardingPresented == true)
}

// MARK: - isRecording parameter

@Test
func isRecordingDefaultsToFalse() {
#expect(RecordButtonTip.isRecording == false)
}

@Test
func isOnboardingPresentedCanBeSetToFalse() {
RecordButtonTip.isOnboardingPresented = false
defer { RecordButtonTip.isOnboardingPresented = true }
#expect(RecordButtonTip.isOnboardingPresented == false)
func isRecordingCanBeSetToTrue() {
RecordButtonTip.isRecording = true
defer { RecordButtonTip.isRecording = false }
#expect(RecordButtonTip.isRecording == true)
}
}
7 changes: 7 additions & 0 deletions DrivelineTests/UITests/StatsPanelTipTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import Testing
@MainActor
struct StatsPanelTipTests {

// MARK: - Rules

@Test
func gatesOnDriveCountRecordingAndOnboarding() {
#expect(StatsPanelTip().rules.count == 3)
}

// MARK: - driveCount parameter

@Test
Expand Down
41 changes: 27 additions & 14 deletions DrivelineUITests/TipUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,67 @@ import XCTest
final class TipUITests: BaseXCTestCase {

// MARK: - Lifecycle

override func setUp() async throws {
enableTips()
try await super.setUp()
}

// MARK: - Tests

@MainActor
func testRecordButtonTipAppearsOnFirstLaunch() throws {
XCTAssertTrue(app.staticTexts["Record a Drive"].waitForExistence(timeout: 3))
func testRecordButtonTipHiddenInEmptyState() throws {
// With no drives recorded the home screen shows the empty state, where the
// record-button tip must stay hidden (the empty state has its own CTA).
XCTAssertFalse(app.staticTexts["Record a Drive"].waitForExistence(timeout: 3))
}

@MainActor
func testRecordButtonTipAppearsAfterFirstDrive() throws {
navigateToHomeScreen()

XCTAssertTrue(app.staticTexts["Record a Drive"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["Tap to start manually tracking a new journey."].exists)
}

@MainActor
func testStatsPanelTipAppearsAfterThreeDrivesRecorded() throws {
closeRecordButtonTip()
navigatePastEmptyState()

XCTAssertTrue(app.staticTexts["RecordingBanner"].waitForExistence(timeout: 5))
app.buttons["FinishDriveButton"].tap()


// The record-button tip appears once the first drive exists; dismiss it so the
// popover doesn't intercept the next record taps.
dismissRecordButtonTipIfPresent()

app.buttons["NewDriveButton"].tap()
XCTAssertTrue(app.staticTexts["RecordingBanner"].waitForExistence(timeout: 5))
app.buttons["FinishDriveButton"].tap()

app.buttons["NewDriveButton"].tap()
XCTAssertTrue(app.staticTexts["RecordingBanner"].waitForExistence(timeout: 5))
app.buttons["FinishDriveButton"].tap()

XCTAssertTrue(app.staticTexts["Switch Stats View"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["Tap to toggle between the last 30 days and all time."].waitForExistence(timeout: 5))
}

@MainActor
func testEditDriveTipAppearsOnDriveDetail() throws {
closeRecordButtonTip()
navigateToHomeScreen()
dismissRecordButtonTipIfPresent()

app.buttons["Drive row 0"].tap()

XCTAssertTrue(app.staticTexts["Edit Your Drive"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["Tap the options button to edit the name and other details."].exists)
}

// MARK: - Private

private func closeRecordButtonTip() {
app.buttons["Close"].tap()

private func dismissRecordButtonTipIfPresent() {
if app.staticTexts["Record a Drive"].waitForExistence(timeout: 5) {
app.buttons["Close"].tap()
}
}
}
Loading