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
15 changes: 7 additions & 8 deletions .github/workflows/ios-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ on:
paths:
- 'AllHandsOnDeck/**'
- 'AllHandsOnDeckTests/**'
- 'AllHandsOnDeckUITests/**'
- 'AllHandsOnDeckWatch/**'
- 'project.yml'
- 'scripts/*ci_xcconfig.py'
- '.github/workflows/ios-ci.yml'
pull_request:
paths:
- 'AllHandsOnDeck/**'
- 'AllHandsOnDeckTests/**'
- 'AllHandsOnDeckUITests/**'
- 'AllHandsOnDeckWatch/**'
- 'project.yml'
- 'scripts/*ci_xcconfig.py'
- '.github/workflows/ios-ci.yml'

permissions:
Expand Down Expand Up @@ -62,14 +66,8 @@ jobs:
LIVEKIT_TOKEN_ENDPOINT: ${{ vars.VITE_LIVEKIT_TOKEN_ENDPOINT }}
LIVEKIT_BETA_ENABLED: ${{ vars.VITE_ENABLE_LIVEKIT_BETA }}
run: |
cat > Secrets.xcconfig <<EOF
SUPABASE_URL = ${SUPABASE_URL}
SUPABASE_ANON_KEY = ${SUPABASE_ANON_KEY}
WEB_JOIN_BASE_URL = ${WEB_JOIN_BASE_URL}
TEAM_ID = LPHP8KBWW8
LIVEKIT_TOKEN_ENDPOINT = ${LIVEKIT_TOKEN_ENDPOINT}
LIVEKIT_BETA_ENABLED = ${LIVEKIT_BETA_ENABLED}
EOF
python3 -m unittest discover -s scripts -p 'test_write_ci_xcconfig.py'
python3 scripts/write_ci_xcconfig.py

- name: Generate project
run: xcodegen generate
Expand Down Expand Up @@ -133,6 +131,7 @@ jobs:
-scheme "$SCHEME" \
-destination "${{ steps.sim.outputs.destination }}" \
-only-testing:AllHandsOnDeckUITests/HappyPathUITests/test_homeScreen_hasAllButtons \
-only-testing:AllHandsOnDeckUITests/HappyPathUITests/test_identitySettingsControlsAreAccessible \
-only-testing:AllHandsOnDeckUITests/HappyPathUITests/test_hostChromeButtonsStayWithinSafeLayoutBands \
-only-testing:AllHandsOnDeckUITests/HappyPathUITests/test_navigationBack_fromHostSession \
CODE_SIGNING_ALLOWED=NO \
Expand Down
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ opt_in_rules:
- notification_center_detachment
- operator_usage_whitespace
- sorted_imports

analyzer_rules:
- unused_import

disabled_rules:
Expand Down
3 changes: 3 additions & 0 deletions AllHandsOnDeck/App/AllHandsOnDeckApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ struct AllHandsOnDeckApp: App {
@StateObject private var linkHandler = UniversalLinkHandler()

init() {
if ProcessInfo.processInfo.arguments.contains("-disableAnimations") {
UIView.setAnimationsEnabled(false)
}
UINavigationBar.appearance().tintColor = UIColor(Theme.gold)
}

Expand Down
2 changes: 1 addition & 1 deletion AllHandsOnDeck/Core/Models/PhotoSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct PhotoSession: Identifiable, Hashable, Codable, Sendable {
/// 8-character random ID. Not guessable, short enough to display.
static func makeShortID() -> String {
let alphabet = Array("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") // omits ambiguous chars
return String((0..<8).map { _ in alphabet.randomElement()! })
return String((0..<8).map { _ in alphabet[Int.random(in: alphabet.indices)] })
}

/// Override the join base URL via UserDefaults["joinBaseURL"] or the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class IdentityService: ObservableObject {
/// The rank earned based on accumulated action points.
@Published private(set) var earnedRank: PirateRank = .cabinBoy

private var actionPoints: Int {
private(set) var actionPoints: Int {
get { UserDefaults.standard.integer(forKey: "identity.actionPoints") }
set {
UserDefaults.standard.set(newValue, forKey: "identity.actionPoints")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class MockSessionTransport: SessionTransport {
statusSubject.send(.connecting)
try? await Task.sleep(nanoseconds: 100_000_000)
statusSubject.send(.connected)

if role == .viewer {
// Simulate host sending initial metadata and joining
let mockSession = PhotoSession(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ enum SupabaseFrameBroadcast {
let event: String
let payload: BroadcastPayload?

struct BroadcastPayload: Decodable {
let event: String?
let payload: FramePayload?
}
}

private struct BroadcastPayload: Decodable {
let event: String?
let payload: FramePayload?
}

/// Decodes a raw websocket message into a frame, or `nil` if the message
Expand Down
8 changes: 6 additions & 2 deletions AllHandsOnDeck/Features/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ struct HomeView: View {
}
}
}

Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
activeTab = .host
Expand Down Expand Up @@ -328,6 +328,7 @@ struct HomeView: View {
// MARK: - Ambient Glow

struct AmbientGlowView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var animate = false

var body: some View {
Expand All @@ -343,7 +344,7 @@ struct AmbientGlowView: View {
.frame(width: 300, height: 300)
.blur(radius: 50)
.offset(x: animate ? -40 : -80, y: animate ? -100 : -150)

// Orb 2
Circle()
.fill(RadialGradient(
Expand All @@ -357,11 +358,14 @@ struct AmbientGlowView: View {
.offset(x: animate ? 80 : 40, y: animate ? 150 : 200)
}
.onAppear {
guard !reduceMotion,
!ProcessInfo.processInfo.arguments.contains("-disableAnimations") else { return }
withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) {
animate.toggle()
}
}
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}

Expand Down
71 changes: 39 additions & 32 deletions AllHandsOnDeck/Features/Settings/IdentitySettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ struct IdentitySettingsView: View {
ZStack {
LeopardWallpaperView()
ScrollView {
VStack(spacing: 20) {
VStack(spacing: Spacing.xl) {
rankCard
customNameSection
gameCenterSection
progressSection
}
.padding(20)
.padding(Spacing.xl)
}
}
.navigationTitle(String(localized: "identity.settings.title"))
.navigationTitle(DesignLabels.identitySettingsTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "OK")) { dismiss() }
Button(DesignLabels.done) { dismiss() }
.foregroundStyle(Theme.gold)
.accessibilityIdentifier("identity_done")
}
}
}
Expand All @@ -45,7 +46,7 @@ struct IdentitySettingsView: View {
.font(.system(size: 13, weight: .medium, design: .rounded))
.foregroundStyle(Theme.mist)
.multilineTextAlignment(.center)
.padding(.horizontal, 16)
.padding(.horizontal, Spacing.lg)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 28)
Expand All @@ -54,48 +55,52 @@ struct IdentitySettingsView: View {
}

private var customNameSection: some View {
VStack(alignment: .leading, spacing: 10) {
label("identity.customName.label", image: "person.fill")

TextField(String(localized: "identity.customName.placeholder"), text: $identity.customName)
VStack(alignment: .leading, spacing: Spacing.md) {
label(DesignLabels.identityCustomNameLabel, image: "person.fill")

TextField(DesignLabels.identityCustomNamePlaceholder, text: $identity.customName)
.accessibilityIdentifier("identity_custom_name")
.accessibilityLabel(DesignLabels.identityCustomNameLabel)
.submitLabel(.done)
.onSubmit { nameFieldFocused = false }
.textFieldStyle(.plain)
.foregroundStyle(Theme.bone)
.tint(Theme.gold)
.focused($nameFieldFocused)
.padding(.horizontal, 16)
.frame(height: 48)
.padding(.horizontal, Spacing.lg)
.frame(height: Spacing.buttonHeight)
.background(Color.white.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous))

if !identity.customName.trimmingCharacters(in: .whitespaces).isEmpty {
Text(String(localized: "identity.customName.hint"))
Text(DesignLabels.identityCustomNameHint)
.font(.caption)
.foregroundStyle(Theme.mist)
}
}
}

private var gameCenterSection: some View {
VStack(alignment: .leading, spacing: 10) {
label("identity.gamecenter.label", image: "gamecontroller.fill")
VStack(alignment: .leading, spacing: Spacing.md) {
label(DesignLabels.identityGameCenterLabel, image: "gamecontroller.fill")

HStack {
VStack(alignment: .leading, spacing: 3) {
if gc.isAuthenticated, let alias = gc.alias {
Text(alias)
.font(.system(size: 15, weight: .heavy, design: .rounded))
.foregroundStyle(Theme.bone)
Text(String(localized: "identity.gamecenter.connected"))
Text(DesignLabels.identityGameCenterConnected)
.font(.caption)
.foregroundStyle(Theme.signal)
} else {
Text(String(localized: "identity.gamecenter.disconnected"))
Text(DesignLabels.identityGameCenterDisconnected)
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundStyle(Theme.mist)
}
}
Spacer()
Toggle("", isOn: Binding(
Toggle(DesignLabels.identityGameCenterLabel, isOn: Binding(
get: { identity.useGameCenter },
set: { on in
if on {
Expand All @@ -106,26 +111,27 @@ struct IdentitySettingsView: View {
}
))
.labelsHidden()
.accessibilityIdentifier("identity_game_center")
.tint(Theme.gold)
}
.padding(.horizontal, 16)
.padding(.horizontal, Spacing.lg)
.padding(.vertical, 12)
.background(Color.white.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous))

if identity.useGameCenter && !gc.isAuthenticated {
Text(String(localized: "identity.gamecenter.notSignedIn"))
Text(DesignLabels.identityGameCenterNotSignedIn)
.font(.caption)
.foregroundStyle(Theme.amber)
}
}
}

private var progressSection: some View {
VStack(alignment: .leading, spacing: 10) {
label("identity.progress.label", image: "chart.bar.fill")
VStack(alignment: .leading, spacing: Spacing.md) {
label(DesignLabels.identityProgressLabel, image: "chart.bar.fill")

let points = UserDefaults.standard.integer(forKey: "identity.actionPoints")
let points = identity.actionPoints
let nextRank = PirateRank(rawValue: identity.earnedRank.rawValue + 1)
let nextThreshold = nextRank?.threshold ?? Int.max
let current = identity.earnedRank
Expand All @@ -135,7 +141,7 @@ struct IdentitySettingsView: View {
Text("\(points)")
.font(.system(size: 28, weight: .black, design: .rounded))
.foregroundStyle(Theme.gold)
Text(String(localized: "identity.progress.points"))
Text(DesignLabels.identityProgressPoints)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Theme.mist)
.padding(.top, 6)
Expand All @@ -158,25 +164,26 @@ struct IdentitySettingsView: View {
.tint(Theme.gold)
}
} else {
Text(String(localized: "identity.progress.maxRank"))
Text(DesignLabels.identityProgressMaxRank)
.font(.caption)
.foregroundStyle(Theme.signal)
}
}
.padding(16)
.padding(Spacing.lg)
.background(Color.white.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous))

Text(String(localized: "identity.progress.hint"))
Text(DesignLabels.identityProgressHint)
.font(.caption)
.foregroundStyle(Theme.mist)
}
}

private func label(_ key: String, image: String) -> some View {
Label(String(localized: String.LocalizationValue(key)),
systemImage: image)
private func label(_ title: String, image: String) -> some View {
Label(title, systemImage: image)
.font(.system(size: 13, weight: .heavy, design: .rounded))
.foregroundStyle(Theme.mist)
}
}

#Preview { IdentitySettingsView() }
14 changes: 14 additions & 0 deletions AllHandsOnDeck/Shared/Utilities/DesignLabels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import SwiftUI
/// All 3 surfaces (host, viewer, webapp) reference these for consistency.
/// English is the default language; no localization fallback.
enum DesignLabels {
// MARK: - Identity settings
static let identitySettingsTitle = String(localized: "identity.settings.title")
static let identityCustomNameLabel = String(localized: "identity.customName.label")
static let identityCustomNamePlaceholder = String(localized: "identity.customName.placeholder")
static let identityCustomNameHint = String(localized: "identity.customName.hint")
static let identityGameCenterLabel = String(localized: "identity.gamecenter.label")
static let identityGameCenterConnected = String(localized: "identity.gamecenter.connected")
static let identityGameCenterDisconnected = String(localized: "identity.gamecenter.disconnected")
static let identityGameCenterNotSignedIn = String(localized: "identity.gamecenter.notSignedIn")
static let identityProgressLabel = String(localized: "identity.progress.label")
static let identityProgressPoints = String(localized: "identity.progress.points")
static let identityProgressMaxRank = String(localized: "identity.progress.maxRank")
static let identityProgressHint = String(localized: "identity.progress.hint")

// MARK: - Buttons
static let cancel = "Cancel"
static let close = "Close"
Expand Down
21 changes: 19 additions & 2 deletions AllHandsOnDeckUITests/HappyPathUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ final class HappyPathUITests: XCTestCase {
app.launchArguments = [
"-useMockTransport", "YES",
"-allowWebJoinDefault", "YES",
"-bypassCameraPermission"
"-bypassCameraPermission",
"-disableAnimations"
]
app.launch()
}
Expand All @@ -26,6 +27,22 @@ final class HappyPathUITests: XCTestCase {
app.terminate()
}

func test_identitySettingsControlsAreAccessible() throws {
let settings = app.buttons["Identity settings"]
XCTAssertTrue(settings.waitForExistence(timeout: 5))
settings.tap()
XCTAssertTrue(app.textFields["identity_custom_name"].waitForExistence(timeout: 5))
let gameCenter = app.switches["identity_game_center"]
XCTAssertTrue(gameCenter.exists)
XCTAssertFalse(gameCenter.label.isEmpty)
let screenshot = XCTAttachment(screenshot: app.screenshot())
screenshot.name = "Identity settings"
screenshot.lifetime = .keepAlways
add(screenshot)
app.buttons["identity_done"].tap()
XCTAssertTrue(settings.waitForExistence(timeout: 5))
}

// MARK: - Host: Session Lifecycle ──────────────────────────────────────────

func test_startHostSession_reachesCameraView() throws {
Expand Down Expand Up @@ -242,7 +259,7 @@ final class HappyPathUITests: XCTestCase {
// Initially on Join Crew tab
XCTAssertTrue(app.buttons["Join Session"].waitForExistence(timeout: 5))
XCTAssertTrue(app.buttons["Nearby Sessions"].exists)

// Switch to Captain tab
let captainTab = app.buttons["Captain"]
if captainTab.waitForExistence(timeout: 5) {
Expand Down
Loading
Loading