mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ADD - introduce SkyRide taxi platform
This commit is contained in:
@@ -1022,6 +1022,217 @@ const defaultLocales: LocaleTree = {
|
||||
default: 'The garage request failed.',
|
||||
},
|
||||
},
|
||||
skyride: {
|
||||
name: 'SkyRide',
|
||||
loading: 'Loading SkyRide',
|
||||
unavailable: 'SkyRide unavailable',
|
||||
tryAgain: 'Try Again',
|
||||
mode: {
|
||||
rider: 'Ride',
|
||||
driver: 'Drive',
|
||||
},
|
||||
riderEyebrow: 'Ride anywhere',
|
||||
whereTo: 'Where to?',
|
||||
pickup: 'Pickup',
|
||||
destination: 'Destination',
|
||||
currentLocation: 'Current location',
|
||||
notSelected: 'Not selected',
|
||||
location: 'Choose a location',
|
||||
chooseLocation: {
|
||||
pickup: 'Set pickup',
|
||||
destination: 'Choose destination',
|
||||
},
|
||||
chooseOnMap: 'Choose on map',
|
||||
chooseOnMapBody: 'Tap a precise point on the Los Santos map.',
|
||||
pickOnMap: {
|
||||
pickup: 'Tap your pickup point',
|
||||
destination: 'Tap your destination',
|
||||
},
|
||||
tapMapHint: 'Drag and zoom the map, then tap to place the pin.',
|
||||
mapPoint: 'Map pin',
|
||||
map: 'Map',
|
||||
useGps: 'Use your live position',
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
resetMap: 'Reset map view',
|
||||
savedPlaces: 'Saved places',
|
||||
savedPlace: 'Saved place',
|
||||
quickDestinations: 'Quick destinations',
|
||||
quickLocations: {
|
||||
'legion-square': 'Legion Square',
|
||||
'diamond-casino': 'Diamond Casino',
|
||||
airport: 'Los Santos Airport',
|
||||
vinewood: 'Vinewood',
|
||||
},
|
||||
viewRides: 'View Ride Options',
|
||||
change: 'Change',
|
||||
requestRide: 'Request SkyRide',
|
||||
serviceMeta: '{eta} min away · {seats} seats',
|
||||
distanceMeters: '{distance} m',
|
||||
distanceKilometers: '{distance} km',
|
||||
distanceMiles: '{distance} mi',
|
||||
durationMinutes: '{minutes} min',
|
||||
fare: 'Trip fare',
|
||||
calculatedFare: 'Calculated',
|
||||
customFare: 'Own price',
|
||||
calculatedFareBody: 'Distance, time, and selected service included.',
|
||||
customFareRange: 'Allowed range: {minimum} to {maximum}',
|
||||
applyCustomFare: 'Apply Own Price',
|
||||
perKilometer: '{price} / km',
|
||||
perMile: '{price} / mi',
|
||||
rideStatus: 'Ride status',
|
||||
services: {
|
||||
taxi: { name: 'Taxi' },
|
||||
comfort: { name: 'Comfort' },
|
||||
xl: { name: 'XL' },
|
||||
premium: { name: 'Premium' },
|
||||
},
|
||||
ride: 'SkyRide',
|
||||
driver: {
|
||||
online: 'You are online',
|
||||
offline: 'You are offline',
|
||||
onlineBody: 'New ride requests can reach you now.',
|
||||
offlineBody: 'Go online when you are ready to drive.',
|
||||
toggleStatus: 'Toggle driver status',
|
||||
today: 'Today',
|
||||
requests: 'Nearby requests',
|
||||
accept: 'Accept Ride',
|
||||
noRequests: 'No nearby requests',
|
||||
noRequestsBody: 'New requests will appear here automatically.',
|
||||
goOnline: 'Go online to receive rides',
|
||||
goOnlineBody: 'Switch your driver status on to see nearby requests.',
|
||||
statusBody: {
|
||||
searching: 'A passenger is waiting for a driver.',
|
||||
accepted: 'Navigate to the passenger pickup.',
|
||||
driver_arriving: 'You are heading to the passenger.',
|
||||
arrived: 'Confirm when the passenger is in the vehicle.',
|
||||
in_progress: 'Follow the route to the destination.',
|
||||
completed: 'The ride has been completed.',
|
||||
cancelled: 'This ride was cancelled.',
|
||||
},
|
||||
actions: {
|
||||
arrive: 'I Have Arrived',
|
||||
start: 'Start Ride',
|
||||
complete: 'Complete Ride',
|
||||
},
|
||||
},
|
||||
passenger: 'Passenger',
|
||||
vehicle: 'Vehicle',
|
||||
navigate: 'Navigate',
|
||||
call: 'Call',
|
||||
message: 'Message',
|
||||
cancelRide: 'Cancel Ride',
|
||||
cancelTitle: 'Cancel this ride?',
|
||||
cancelBody:
|
||||
'Your driver or passenger will be notified. A cancellation fee may apply.',
|
||||
safetyNote: 'Trip details are shared only while this ride is active.',
|
||||
waypointSet: 'Route added to your GPS.',
|
||||
status: {
|
||||
searching: 'Finding a driver',
|
||||
accepted: 'Driver found',
|
||||
driver_arriving: 'Driver en route',
|
||||
arrived: 'Driver arrived',
|
||||
in_progress: 'On the way',
|
||||
completed: 'Completed',
|
||||
cancelled: 'Cancelled',
|
||||
},
|
||||
statusBody: {
|
||||
searching: 'We are matching you with a nearby driver.',
|
||||
accepted: 'Your driver is preparing to pick you up.',
|
||||
driver_arriving: 'Your driver is on the way to your pickup.',
|
||||
arrived: 'Your driver is waiting at the pickup point.',
|
||||
in_progress: 'You are on the way to your destination.',
|
||||
completed: 'You have arrived at your destination.',
|
||||
cancelled: 'This ride is no longer active.',
|
||||
},
|
||||
navigation: 'SkyRide navigation',
|
||||
tabs: {
|
||||
home: 'Home',
|
||||
rides: 'Rides',
|
||||
activity: 'Activity',
|
||||
messages: 'Messages',
|
||||
profile: 'Profile',
|
||||
},
|
||||
rides: 'Your Rides',
|
||||
ridesBody: 'Review completed and cancelled rides.',
|
||||
noRides: 'No rides yet',
|
||||
noRidesBody: 'Completed and cancelled rides will appear here.',
|
||||
activity: 'Activity',
|
||||
activityBody: 'Recent trip updates and milestones.',
|
||||
noActivity: 'No recent activity',
|
||||
noActivityBody: 'Ride updates will appear here.',
|
||||
messages: 'Messages',
|
||||
messagesBody: 'Contact your active driver or passenger.',
|
||||
noMessages: 'No active contact',
|
||||
noMessagesBody: 'Book or accept a ride to contact the other person.',
|
||||
openMessages: 'Open Messages',
|
||||
account: 'Account',
|
||||
paymentMethod: 'Payment Method',
|
||||
paymentMethods: {
|
||||
bank: 'Bank',
|
||||
cash: 'Cash',
|
||||
default: 'Payment method',
|
||||
},
|
||||
safety: 'Safety Center',
|
||||
safetyBody: 'Help, trip sharing, and emergency information',
|
||||
driverMode: 'Driver Mode',
|
||||
driverModeBody: 'Accept ride requests and track your earnings',
|
||||
completedRides: 'Completed',
|
||||
cancelledRides: 'Cancelled',
|
||||
acceptance: 'Acceptance',
|
||||
rating: 'Rating',
|
||||
notAvailable: 'N/A',
|
||||
memberSince: 'SkyRide member since {date}',
|
||||
rideComplete: 'Ride Complete',
|
||||
rateRide: 'Rate your ride',
|
||||
rateRideBody: 'How was your trip? Your feedback helps every rider.',
|
||||
ratingValue: '{rating} stars',
|
||||
tip: 'Add a tip',
|
||||
noTip: 'No tip',
|
||||
comment: 'Comment',
|
||||
commentPlaceholder: 'Share an optional note...',
|
||||
submitRating: 'Submit Rating',
|
||||
notNow: 'Not Now',
|
||||
ratingSaved: 'Thanks for your feedback.',
|
||||
errors: {
|
||||
skyride_unavailable: 'SkyRide is currently unavailable.',
|
||||
invalid_location: 'Choose a valid pickup and destination.',
|
||||
pickup_too_far: 'Move closer to the selected pickup point.',
|
||||
invalid_route: 'No suitable route was found for this ride.',
|
||||
invalid_custom_fare: 'Choose an own price inside the allowed range.',
|
||||
quote_not_found: 'These ride options are no longer available.',
|
||||
quote_expired: 'These ride options expired. Request a new quote.',
|
||||
active_ride_exists: 'Finish or cancel your active ride first.',
|
||||
invalid_driver_status: 'That driver status is not available.',
|
||||
driver_not_eligible: 'This account is not eligible for Driver Mode.',
|
||||
driver_offline: 'Go online before accepting a ride.',
|
||||
driver_unavailable: 'That driver is no longer available.',
|
||||
driver_vehicle_required:
|
||||
"Sit in the driver's seat of a vehicle before accepting a ride.",
|
||||
ride_not_found: 'This ride is no longer available.',
|
||||
invalid_ride_status: 'This action is not available for the ride.',
|
||||
not_ride_participant: 'You are not part of this ride.',
|
||||
driver_too_far: 'Move closer to the required ride location.',
|
||||
passenger_too_far: 'The passenger is not close enough to start.',
|
||||
destination_too_far:
|
||||
'Move closer to the destination to complete the ride.',
|
||||
payout_failed: 'The driver payout could not be completed.',
|
||||
refund_failed: 'The ride refund could not be completed.',
|
||||
invalid_cancel_reason: 'Choose a valid cancellation reason.',
|
||||
invalid_rating: 'Choose a rating from one to five stars.',
|
||||
invalid_tip: 'Choose a valid tip amount.',
|
||||
invalid_comment: 'The rating comment is not valid.',
|
||||
tip_failed: 'The tip could not be processed.',
|
||||
insufficient_funds:
|
||||
'Your selected payment method has insufficient funds.',
|
||||
rate_limited: 'Please wait a moment before trying again.',
|
||||
device_not_open: 'Open the phone again to use SkyRide.',
|
||||
device_not_owned: 'This phone does not belong to this character.',
|
||||
device_locked: 'Unlock this phone to use SkyRide.',
|
||||
request_failed: 'SkyRide could not complete the request.',
|
||||
default: 'SkyRide could not complete the request.',
|
||||
},
|
||||
},
|
||||
calculator: { name: 'Calculator' },
|
||||
snake: {
|
||||
name: 'Snake',
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSkyRideStore } from '@/stores/skyride'
|
||||
import type {
|
||||
SkyRideBootstrap,
|
||||
SkyRideLocation,
|
||||
SkyRideQuote,
|
||||
SkyRideRide,
|
||||
} from '@/types/skyride'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
|
||||
const mockNuiCall = vi.mocked(nuiCall)
|
||||
const pickup: SkyRideLocation = {
|
||||
coords: { x: -1037.7, y: -2737.8, z: 20.1 },
|
||||
label: 'Los Santos International',
|
||||
}
|
||||
const destination: SkyRideLocation = {
|
||||
coords: { x: 215.8, y: -810.1, z: 30.7 },
|
||||
label: 'Pillbox Hill',
|
||||
}
|
||||
const ride: SkyRideRide = {
|
||||
createdAt: 1_755_000_000,
|
||||
currency: '$',
|
||||
destination,
|
||||
driver: null,
|
||||
id: 'ride-1',
|
||||
pickup,
|
||||
price: 48,
|
||||
serviceClass: 'comfort',
|
||||
status: 'searching',
|
||||
updatedAt: 1_755_000_000,
|
||||
}
|
||||
const bootstrap: SkyRideBootstrap = {
|
||||
activeRide: null,
|
||||
availableRequests: [ride],
|
||||
driverEligible: true,
|
||||
driverOnline: false,
|
||||
history: [],
|
||||
pendingRating: null,
|
||||
profile: {
|
||||
acceptanceRate: 94,
|
||||
avatarUrl: null,
|
||||
cancelledRides: 2,
|
||||
completedRides: 128,
|
||||
currency: '$',
|
||||
defaultPaymentMethod: 'Bank',
|
||||
earningsToday: 320,
|
||||
id: 'profile-1',
|
||||
memberSince: 1_700_000_000,
|
||||
name: 'Alex Morgan',
|
||||
rating: 4.9,
|
||||
},
|
||||
quickLocations: [destination],
|
||||
}
|
||||
const quote: SkyRideQuote = {
|
||||
destination,
|
||||
distance: 5.2,
|
||||
distanceMeters: 5200,
|
||||
distanceUnit: 'kilometer',
|
||||
durationSeconds: 640,
|
||||
expiresAt: 1_755_000_120,
|
||||
options: [
|
||||
{
|
||||
available: true,
|
||||
calculatedPrice: 48,
|
||||
currency: '$',
|
||||
etaMinutes: 4,
|
||||
fareMode: 'calculated',
|
||||
maximumCustomPrice: 144,
|
||||
minimumCustomPrice: 24,
|
||||
price: 48,
|
||||
pricePerDistanceUnit: 24,
|
||||
quoteId: 'quote-comfort',
|
||||
seats: 4,
|
||||
serviceClass: 'comfort',
|
||||
},
|
||||
],
|
||||
pickup,
|
||||
}
|
||||
|
||||
describe('SkyRide store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockNuiCall.mockReset()
|
||||
})
|
||||
|
||||
it('hydrates the complete server-authoritative snapshot', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: bootstrap, success: true })
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.quote = quote
|
||||
|
||||
expect(await skyride.bootstrap()).toBe(true)
|
||||
expect(skyride.profile).toEqual(bootstrap.profile)
|
||||
expect(skyride.driverEligible).toBe(true)
|
||||
expect(skyride.availableRequests).toEqual([ride])
|
||||
expect(skyride.quote).toBeNull()
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:bootstrap')
|
||||
})
|
||||
|
||||
it('requests a quote with pickup and destination coordinates', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: quote, success: true })
|
||||
const skyride = useSkyRideStore()
|
||||
|
||||
expect((await skyride.createQuote(pickup, destination)).success).toBe(true)
|
||||
expect(skyride.quote).toEqual(quote)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:quote', {
|
||||
destination,
|
||||
pickup,
|
||||
})
|
||||
})
|
||||
|
||||
it('books with the opaque quote id and never sends a client price', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { activeRide: ride },
|
||||
success: true,
|
||||
})
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.quote = quote
|
||||
|
||||
const response = await skyride.requestRide(quote.options[0])
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(skyride.activeRide).toEqual(ride)
|
||||
expect(skyride.quote).toBeNull()
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:request', {
|
||||
quoteId: 'quote-comfort',
|
||||
})
|
||||
})
|
||||
|
||||
it('asks the server to bind a validated own price to a new quote', async () => {
|
||||
const customQuote = {
|
||||
...quote,
|
||||
options: [
|
||||
{
|
||||
...quote.options[0],
|
||||
fareMode: 'custom' as const,
|
||||
price: 60,
|
||||
quoteId: 'quote-comfort-custom',
|
||||
},
|
||||
],
|
||||
}
|
||||
mockNuiCall.mockResolvedValueOnce({ data: customQuote, success: true })
|
||||
const skyride = useSkyRideStore()
|
||||
|
||||
await skyride.createQuote(pickup, destination, {
|
||||
price: 60,
|
||||
serviceClass: 'comfort',
|
||||
})
|
||||
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:quote', {
|
||||
customFare: { price: 60, serviceClass: 'comfort' },
|
||||
destination,
|
||||
pickup,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the active ride when a lifecycle action is rejected', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
error: 'ride_state_changed',
|
||||
success: false,
|
||||
})
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.activeRide = ride
|
||||
|
||||
await skyride.performRideAction('start', ride.id)
|
||||
|
||||
expect(skyride.activeRide).toEqual(ride)
|
||||
expect(skyride.error).toBe('ride_state_changed')
|
||||
})
|
||||
|
||||
it('applies live snapshots including an explicit cleared ride', () => {
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.activeRide = ride
|
||||
skyride.driverOnline = false
|
||||
|
||||
skyride.applyUpdate({ activeRide: null, driverOnline: true })
|
||||
|
||||
expect(skyride.activeRide).toBeNull()
|
||||
expect(skyride.driverOnline).toBe(true)
|
||||
})
|
||||
|
||||
it('submits rating details and clears the pending rating on success', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { history: [{ ...ride, status: 'completed' }] },
|
||||
success: true,
|
||||
})
|
||||
const skyride = useSkyRideStore()
|
||||
skyride.pendingRating = { ...ride, status: 'completed' }
|
||||
|
||||
await skyride.rateRide(ride.id, 5, 10, 'Excellent')
|
||||
|
||||
expect(skyride.pendingRating).toBeNull()
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('skyride:rate', {
|
||||
comment: 'Excellent',
|
||||
rating: 5,
|
||||
rideId: ride.id,
|
||||
tip: 10,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
SkyRideBootstrap,
|
||||
SkyRideCoordinates,
|
||||
SkyRideCustomFareInput,
|
||||
SkyRideHistoryResponse,
|
||||
SkyRideLocation,
|
||||
SkyRideQuote,
|
||||
SkyRideQuoteOption,
|
||||
SkyRideRide,
|
||||
SkyRideStateUpdate,
|
||||
} from '@/types/skyride'
|
||||
import { nuiCall, type NuiResponse } from '@/utils/nui'
|
||||
|
||||
type RideAction = 'accept' | 'arrive' | 'start' | 'complete'
|
||||
|
||||
export const useSkyRideStore = defineStore('skyride', {
|
||||
state: () => ({
|
||||
activeRide: null as SkyRideRide | null,
|
||||
availableRequests: [] as SkyRideRide[],
|
||||
driverEligible: false,
|
||||
driverOnline: false,
|
||||
error: '',
|
||||
history: [] as SkyRideRide[],
|
||||
isActionPending: false,
|
||||
isLoading: false,
|
||||
pendingRating: null as SkyRideRide | null,
|
||||
profile: null as SkyRideBootstrap['profile'] | null,
|
||||
quickLocations: [] as SkyRideLocation[],
|
||||
quote: null as SkyRideQuote | null,
|
||||
}),
|
||||
actions: {
|
||||
applyUpdate(update: SkyRideStateUpdate): void {
|
||||
if (update.activeRide !== undefined) this.activeRide = update.activeRide
|
||||
if (update.availableRequests !== undefined)
|
||||
this.availableRequests = update.availableRequests
|
||||
if (update.driverOnline !== undefined)
|
||||
this.driverOnline = update.driverOnline
|
||||
if (update.history !== undefined) this.history = update.history
|
||||
if (update.pendingRating !== undefined)
|
||||
this.pendingRating = update.pendingRating
|
||||
if (update.profile !== undefined) this.profile = update.profile
|
||||
},
|
||||
async bootstrap(): Promise<boolean> {
|
||||
this.isLoading = true
|
||||
const response = await nuiCall<SkyRideBootstrap>('skyride:bootstrap')
|
||||
this.isLoading = false
|
||||
if (!response.success || !response.data) {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
|
||||
this.activeRide = response.data.activeRide
|
||||
this.availableRequests = response.data.availableRequests
|
||||
this.driverEligible = response.data.driverEligible
|
||||
this.driverOnline = response.data.driverOnline
|
||||
this.history = response.data.history
|
||||
this.pendingRating = response.data.pendingRating
|
||||
this.profile = response.data.profile
|
||||
this.quickLocations = response.data.quickLocations
|
||||
this.quote = null
|
||||
this.error = ''
|
||||
return true
|
||||
},
|
||||
async loadHistory(): Promise<boolean> {
|
||||
const response = await nuiCall<SkyRideHistoryResponse>('skyride:history')
|
||||
if (!response.success || !response.data) {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
return false
|
||||
}
|
||||
this.history = response.data.items
|
||||
this.error = ''
|
||||
return true
|
||||
},
|
||||
async createQuote(
|
||||
pickup: SkyRideLocation,
|
||||
destination: SkyRideLocation,
|
||||
customFare?: SkyRideCustomFareInput,
|
||||
): Promise<NuiResponse<SkyRideQuote>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideQuote>('skyride:quote', {
|
||||
...(customFare ? { customFare } : {}),
|
||||
destination,
|
||||
pickup,
|
||||
})
|
||||
this.isActionPending = false
|
||||
if (response.success && response.data) {
|
||||
this.quote = response.data
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async requestRide(
|
||||
option: Pick<SkyRideQuoteOption, 'quoteId'>,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>('skyride:request', {
|
||||
quoteId: option.quoteId,
|
||||
})
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.quote = null
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async setDriverStatus(
|
||||
online: boolean,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>(
|
||||
'skyride:set-driver-status',
|
||||
{ online },
|
||||
)
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
this.driverOnline = response.data?.driverOnline ?? online
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async performRideAction(
|
||||
action: RideAction,
|
||||
rideId: string,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>(`skyride:${action}`, {
|
||||
rideId,
|
||||
})
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async cancelRide(
|
||||
rideId: string,
|
||||
reason?: string,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>('skyride:cancel', {
|
||||
rideId,
|
||||
...(reason ? { reason } : {}),
|
||||
})
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async rateRide(
|
||||
rideId: string,
|
||||
rating: number,
|
||||
tip: number,
|
||||
comment: string,
|
||||
): Promise<NuiResponse<SkyRideStateUpdate>> {
|
||||
this.isActionPending = true
|
||||
const response = await nuiCall<SkyRideStateUpdate>('skyride:rate', {
|
||||
comment,
|
||||
rating,
|
||||
rideId,
|
||||
tip,
|
||||
})
|
||||
this.isActionPending = false
|
||||
if (response.success) {
|
||||
if (response.data) this.applyUpdate(response.data)
|
||||
this.pendingRating = null
|
||||
this.error = ''
|
||||
} else {
|
||||
this.error = response.error ?? 'request_failed'
|
||||
}
|
||||
return response
|
||||
},
|
||||
async getPlayerCoordinates(): Promise<
|
||||
NuiResponse<{ coords: SkyRideCoordinates }>
|
||||
> {
|
||||
return nuiCall<{ coords: SkyRideCoordinates }>(
|
||||
'skyride:get-player-coords',
|
||||
)
|
||||
},
|
||||
async setWaypoint(coords: SkyRideCoordinates): Promise<NuiResponse> {
|
||||
return nuiCall('map:setWaypoint', { coords })
|
||||
},
|
||||
clearQuote(): void {
|
||||
this.quote = null
|
||||
this.error = ''
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user