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
1 change: 1 addition & 0 deletions .github/workflows/revyl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ jobs:
python-version: '3.12'
- run: python -m pip install PyYAML==6.0.3
- run: python scripts/test_revyl_config.py
- run: python -m unittest discover -s apps/rides/scripts -p 'test_*.py'
2 changes: 1 addition & 1 deletion .github/workflows/rides.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Rides demo
name: Rides
on:
push:
branches: [main]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
__pycache__/
build/
.generated/
DerivedData/
Expand Down
9 changes: 4 additions & 5 deletions apps/rides/.revyl/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ pr_review:
kind: revyl
always_verify:
- "Scope this proof to the Rides application selected by this project's rides profile. Verify all applicable Rides changes and the checks below, and report any observed Rides defect. Eats has its own root project, build, session, and proof at .revyl/config.yaml. Do not require Eats ordering screens inside the Rides binary; they belong to the separate Eats proof, not an unmet Rides verification requirement."
- A fresh Rides session opens Home with the Revyl sign-in accepted Rider banner, without opening Eats or asking for real authentication.
- The hosted Convex backend is prefilled in Demo connection. Create a new private Rider room, then Continue as Rider. Keep invitations hidden and never include invitation codes, session tokens, or credentials in public evidence.
- Open Where to, select an offline destination, choose a vehicle and demo payment label, and confirm pickup. Verify the hosted request reaches Finding your ride; without a second online driver it must not invent an acceptance or charge. Cancel the requested trip and verify a zero-charge Cancelled history entry. Do not enter real payment information.
- Home, Services, Activity, and Account remain reachable after the cancellation. Open saved places and wallet from Account using only demo data.
- Driver acceptance, two-way chat, arrival, start, completion, receipts, rating, and tips require a second Rides phone joined to the same private room. Do not report those paired behaviors as passed from this single-phone proof.
- A fresh Rides session opens the normal Rider Home signed into a newly registered Convex database account. The session hook supplies its real account session; no connection screen, invitation, role switch, or test banner should be visible. Never publish account session tokens, launch values, passwords, or credentials.
- Open Where to, select a destination, choose a vehicle and payment preference, and confirm pickup. Verify the hosted request reaches Finding your ride; without a second online driver it must not invent an acceptance or charge. Cancel the request and verify a zero-charge Cancelled history entry. Do not enter real payment information.
- Home, Services, Activity, and Account remain reachable after cancellation. Account must show the registered profile. Open saved places and wallet; payments, payouts, live GPS, and driver verification are unavailable and must not be reported as working integrations.
- Driver acceptance, two-way chat, arrival, start, completion, receipts, rating, and tip preferences require a second Rides phone signed into a separate real database driver account in the same isolated service area. Do not report paired behaviors as passed from this single-phone proof.
strict_ci_check:
build: true
12 changes: 6 additions & 6 deletions apps/rides/App/RidesDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI

@main
struct RidesDemoApp: App {
@StateObject private var store = RideStore()
@StateObject private var store = RideStore(accountOnly: true)
var body: some Scene {
WindowGroup {
RideRootView()
Expand All @@ -18,9 +18,9 @@ struct RideRootView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
VStack(spacing: 0) {
if let status = store.revylStatus {
Text(status).font(.caption2).frame(maxWidth: .infinity).padding(5).background(Color.yellow.opacity(0.22))
}
if store.account == nil || store.showSignIn {
AccountSignInView()
} else {
if let error = store.errorMessage {
HStack(alignment: .top) {
Image(systemName: "exclamationmark.circle.fill")
Expand All @@ -44,11 +44,11 @@ struct RideRootView: View {
NavigationStack { RideActivityView() }.tabItem { Label("Activity", systemImage: "receipt.fill") }.tag(2)
NavigationStack { RideAccountView() }.tabItem { Label("Account", systemImage: "person.fill") }.tag(3)
}.tint(.black)
}
}
.sheet(isPresented: $store.showConnection) { DemoConnectionView().environmentObject(store) }
.fullScreenCover(isPresented: $store.showBooking) { BookingFlowView().environmentObject(store) }
.task(id: scenePhase == .active ? store.credentials?.token : nil) {
if scenePhase == .active { await store.runUpdates() }
if scenePhase == .active, store.account != nil { await store.runUpdates() }
}
}
}
39 changes: 32 additions & 7 deletions apps/rides/Core/ConvexRideAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ final class ConvexRideAPIClient: RideAPI {
return Data(bytes).base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
}

static func module(for token: String) -> String { token.hasPrefix("user.") ? "dispatch" : "rides" }

func signUp(_ registration: RideRegistration) async throws -> RideSession {
do {
return try await client.action("accounts:signUp", with: ["name": registration.name, "email": registration.email, "password": registration.password, "role": registration.role.rawValue, "vehicle": registration.vehicle, "plate": registration.plate])
} catch { throw Self.apiError(error) }
}

func signIn(email: String, password: String) async throws -> RideSession {
do { return try await client.action("accounts:signIn", with: ["email": email, "password": password]) }
catch { throw Self.apiError(error) }
}

func resume(token: String) async throws -> RideSession {
do {
for try await session in client.subscribe(to: "accounts:resume", with: ["token": token], yielding: RideSession.self).values { return session }
throw RideAPIError(status: 0, message: "The sign-in connection closed. Try again.")
} catch { throw Self.apiError(error) }
}

func signOut(token: String) async throws {
do { let _: Bool = try await client.mutation("accounts:signOut", with: ["token": token]) }
catch { throw Self.apiError(error) }
}

func create(name: String) async throws -> RideSession {
try await mutate("createRoom", args: ["name": name, "token": Self.freshToken()])
}
Expand All @@ -36,28 +61,28 @@ final class ConvexRideAPIClient: RideAPI {

func state(token: String) async throws -> RideRoomState {
do {
for try await state in client.subscribe(to: "rides:state", with: ["token": token], yielding: RideRoomState.self).values {
for try await state in client.subscribe(to: Self.module(for: token) + ":state", with: ["token": token], yielding: RideRoomState.self).values {
return state
}
throw RideAPIError(status: 0, message: "The shared ride connection closed. Try again.")
} catch { throw Self.apiError(error) }
}

func availability(token: String, online: Bool) async throws -> RideRoomState {
try await mutate("availability", args: ["token": token, "online": online])
try await mutate("availability", module: Self.module(for: token), args: ["token": token, "online": online])
}

func book(token: String, request: TripRequest) async throws -> RideRoomState {
try await mutate("book", args: ["token": token, "request": request])
try await mutate("book", module: Self.module(for: token), args: ["token": token, "request": request])
}

func act(token: String, tripID: String, action: RideAction) async throws -> RideRoomState {
try await mutate("act", args: ["token": token, "tripID": tripID, "action": action])
try await mutate("act", module: Self.module(for: token), args: ["token": token, "tripID": tripID, "action": action])
}

func updates(token: String) -> AsyncThrowingStream<RideSyncEvent, Error> {
AsyncThrowingStream { continuation in
let state = client.subscribe(to: "rides:state", with: ["token": token], yielding: RideRoomState.self)
let state = client.subscribe(to: Self.module(for: token) + ":state", with: ["token": token], yielding: RideRoomState.self)
.map(RideSyncEvent.state)
let connection = connectionState
.map(RideSyncEvent.connection)
Expand All @@ -74,8 +99,8 @@ final class ConvexRideAPIClient: RideAPI {
}
}

private func mutate<T: Decodable>(_ name: String, args: [String: ConvexEncodable?]) async throws -> T {
do { return try await client.mutation("rides:" + name, with: args) }
private func mutate<T: Decodable>(_ name: String, module: String = "rides", args: [String: ConvexEncodable?]) async throws -> T {
do { return try await client.mutation(module + ":" + name, with: args) }
catch { throw Self.apiError(error) }
}

Expand Down
20 changes: 18 additions & 2 deletions apps/rides/Core/RideAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ enum RideSyncEvent {

protocol RideAPI {
var baseURL: URL { get }
func signUp(_ registration: RideRegistration) async throws -> RideSession
func signIn(email: String, password: String) async throws -> RideSession
func resume(token: String) async throws -> RideSession
func signOut(token: String) async throws
func create(name: String) async throws -> RideSession
func join(code: String, role: DemoRole, name: String) async throws -> RideSession
func state(token: String) async throws -> RideRoomState
Expand All @@ -26,6 +30,18 @@ protocol RideAPI {
}

extension RideAPI {
func signUp(_ registration: RideRegistration) async throws -> RideSession {
throw RideAPIError(status: 400, message: "Account sign-in is unavailable for this service.")
}
func signIn(email: String, password: String) async throws -> RideSession {
throw RideAPIError(status: 400, message: "Account sign-in is unavailable for this service.")
}
func resume(token: String) async throws -> RideSession {
throw RideAPIError(status: 401, message: "Sign in again to continue.")
}
func signOut(token: String) async throws {
throw RideAPIError(status: 400, message: "Account sign-out is unavailable for this service.")
}
func updates(token: String) -> AsyncThrowingStream<RideSyncEvent, Error> {
AsyncThrowingStream { continuation in
let task = Task {
Expand Down Expand Up @@ -68,7 +84,7 @@ struct RideAPIClient: RideAPI {
parts.user == nil, parts.password == nil, parts.query == nil, parts.fragment == nil,
parts.path.isEmpty || parts.path == "/",
parts.scheme == "https" || (parts.scheme == "http" && ["localhost", "127.0.0.1", "[::1]"].contains(host)) else {
throw RideAPIError(status: 0, message: "Enter the HTTPS address of your demo server, without a path. Localhost HTTP is allowed for development.")
throw RideAPIError(status: 0, message: "The service address must use HTTPS without a path. Localhost HTTP is allowed for development.")
}
return url
}
Expand Down Expand Up @@ -104,7 +120,7 @@ struct RideAPIClient: RideAPI {
let (data, response) = try await session.data(for: request)
guard let response = response as? HTTPURLResponse else { throw RideAPIError(status: 0, message: "The server didn't return an HTTP response.") }
guard (200..<300).contains(response.statusCode) else {
let message = (try? JSONDecoder().decode(Failure.self, from: data).error) ?? "The demo server couldn't complete this request."
let message = (try? JSONDecoder().decode(Failure.self, from: data).error) ?? "The service couldn't complete this request."
throw RideAPIError(status: response.statusCode, message: message)
}
return try JSONDecoder().decode(T.self, from: data)
Expand Down
34 changes: 33 additions & 1 deletion apps/rides/Core/RideModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ enum DemoPayment: String, Codable, CaseIterable, Identifiable {
case cash = "Demo Cash"
case wallet = "Demo Wallet"
var id: String { rawValue }
var title: String {
switch self {
case .visa: return "Card"
case .cash: return "Cash"
case .wallet: return "Wallet"
}
}
static func displayName(_ value: String) -> String { DemoPayment(rawValue: value)?.title ?? value }
var icon: String { self == .visa ? "creditcard.fill" : self == .cash ? "banknote.fill" : "wallet.pass.fill" }
}

Expand All @@ -70,7 +78,7 @@ enum TripStatus: String, Codable {
case .accepted: return "Your driver is on the way"
case .arriving: return "Your driver has arrived"
case .inProgress: return "You're on your way"
case .completed: return "Thanks for riding, Alex"
case .completed: return "Thanks for riding"
case .cancelled: return "Your ride was cancelled"
}
}
Expand Down Expand Up @@ -128,13 +136,33 @@ struct RideRoomState: Codable, Equatable {
var activeTrip: DemoTrip? { trips.first { $0.status.isActive } }
}

struct RideAccount: Codable, Equatable {
let id: String
let name: String
let email: String
let role: DemoRole
let vehicle: String?
let plate: String?
}

struct RideRegistration {
let name: String
let email: String
let password: String
let role: DemoRole
let vehicle: String?
let plate: String?
}

struct RideSession: Codable, Equatable {
let roomCode: String
let participantId: String
let token: String
let role: DemoRole
let name: String
let state: RideRoomState
var account: RideAccount? = nil
var expiresAt: Double? = nil
}

struct StoredRideSession: Codable {
Expand All @@ -144,6 +172,8 @@ struct StoredRideSession: Codable {
let token: String
let role: DemoRole
let name: String
let account: RideAccount?
let expiresAt: Double?

init(_ session: RideSession, baseURL: URL) {
self.baseURL = baseURL
Expand All @@ -152,6 +182,8 @@ struct StoredRideSession: Codable {
token = session.token
role = session.role
name = session.name
account = session.account
expiresAt = session.expiresAt
}
}

Expand Down
Loading
Loading