mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 09:18:57 +00:00
Merge branch 'dev' into weather-app
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="skyride-bg" x1="62" y1="26" x2="446" y2="488" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFE66A"/>
|
||||
<stop offset="0.48" stop-color="#FFC62E"/>
|
||||
<stop offset="1" stop-color="#FF9500"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="skyride-car" x1="154" y1="192" x2="352" y2="388" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="1" stop-color="#ECEEF2"/>
|
||||
</linearGradient>
|
||||
<filter id="skyride-shadow" x="88" y="102" width="346" height="340" filterUnits="userSpaceOnUse">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#402400" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="512" height="512" rx="112" fill="url(#skyride-bg)"/>
|
||||
<path d="M63 128C146 88 218 112 251 169C285 226 362 224 452 163" fill="none" stroke="#171717" stroke-opacity="0.15" stroke-width="20" stroke-linecap="round" stroke-dasharray="2 40"/>
|
||||
<path d="M75 405C145 347 191 354 242 389C292 424 357 435 441 389" fill="none" stroke="#171717" stroke-opacity="0.12" stroke-width="18" stroke-linecap="round" stroke-dasharray="2 38"/>
|
||||
|
||||
<g filter="url(#skyride-shadow)">
|
||||
<path d="M370 104C329 104 296 137 296 178C296 236 370 293 370 293C370 293 444 236 444 178C444 137 411 104 370 104Z" fill="#171717"/>
|
||||
<circle cx="370" cy="178" r="28" fill="#FFD137"/>
|
||||
|
||||
<path d="M183 199C190 181 207 169 226 169H286C305 169 322 181 329 199L350 251C371 260 385 281 385 305V371C385 384 375 394 362 394H342C329 394 319 384 319 371V360H193V371C193 384 183 394 170 394H150C137 394 127 384 127 371V305C127 281 141 260 162 251L183 199Z" fill="url(#skyride-car)"/>
|
||||
<path d="M207 205C211 196 220 190 230 190H282C292 190 301 196 305 205L320 244H192L207 205Z" fill="#20232A"/>
|
||||
<path d="M238 151H274C282 151 289 157 289 166V179H223V166C223 157 230 151 238 151Z" fill="#171717"/>
|
||||
<rect x="235" y="158" width="42" height="13" rx="6.5" fill="#FFD137"/>
|
||||
<path d="M155 279H357C364 279 370 285 370 292V335C370 342 364 348 357 348H155C148 348 142 342 142 335V292C142 285 148 279 155 279Z" fill="#FFFFFF"/>
|
||||
<path d="M158 291L205 300L188 325H156C150 325 146 321 146 315V301C146 294 151 290 158 291Z" fill="#171717"/>
|
||||
<path d="M354 291L307 300L324 325H356C362 325 366 321 366 315V301C366 294 361 290 354 291Z" fill="#171717"/>
|
||||
<rect x="224" y="312" width="64" height="12" rx="6" fill="#171717"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -41,6 +41,12 @@ describe('app registry', () => {
|
||||
labelKey: 'Apps.garage.name',
|
||||
route: '/apps/garage',
|
||||
})
|
||||
expect(PHONE_APPS.find((app) => app.id === 'skyride')).toMatchObject({
|
||||
category: 'utilities',
|
||||
gridOrder: 25,
|
||||
labelKey: 'Apps.skyride.name',
|
||||
route: '/apps/skyride',
|
||||
})
|
||||
expect(PHONE_APPS.find((app) => app.id === 'calendar')).toMatchObject({
|
||||
gridOrder: 21,
|
||||
labelKey: 'Apps.calendar.name',
|
||||
@@ -109,6 +115,7 @@ describe('app registry', () => {
|
||||
expect(isPhoneAppId('camera')).toBe(true)
|
||||
expect(isPhoneAppId('photos')).toBe(true)
|
||||
expect(isPhoneAppId('clock')).toBe(true)
|
||||
expect(isPhoneAppId('skyride')).toBe(true)
|
||||
expect(
|
||||
PHONE_APPS.filter((app) => app.category === 'games').map((app) => app.id),
|
||||
).toEqual([
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Blocks,
|
||||
Camera,
|
||||
CarFront,
|
||||
CarTaxiFront,
|
||||
CalendarDays,
|
||||
Clock3,
|
||||
Gamepad2,
|
||||
@@ -58,6 +59,7 @@ import localPagesIcon from '@/assets/img/app-icons/local-pages.webp'
|
||||
import flareIcon from '@/assets/img/app-icons/flare.svg'
|
||||
import flipTokIcon from '@/assets/img/app-icons/fliptok.webp'
|
||||
import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
|
||||
import skyRideIcon from '@/assets/img/app-icons/skyride.svg'
|
||||
import type {
|
||||
LaunchablePhoneAppDefinition,
|
||||
LaunchablePhoneAppId,
|
||||
@@ -219,6 +221,20 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
labelKey: 'Apps.map.name',
|
||||
route: '/apps/map',
|
||||
},
|
||||
{
|
||||
category: 'utilities',
|
||||
component: markRaw(
|
||||
defineAsyncComponent(() => import('@/views/apps/SkyRideApp.vue')),
|
||||
),
|
||||
dockOrder: null,
|
||||
gridOrder: 25,
|
||||
icon: markRaw(CarTaxiFront),
|
||||
iconClass: 'app-icon--skyride',
|
||||
iconImage: skyRideIcon,
|
||||
id: 'skyride',
|
||||
labelKey: 'Apps.skyride.name',
|
||||
route: '/apps/skyride',
|
||||
},
|
||||
{
|
||||
category: 'social',
|
||||
component: markRaw(
|
||||
|
||||
@@ -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 = ''
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -30,6 +30,7 @@ export type PhoneAppId =
|
||||
| 'flare'
|
||||
| 'fliptok'
|
||||
| 'picstagram'
|
||||
| 'skyride'
|
||||
|
||||
export type LaunchablePhoneAppId = PhoneAppId
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
export type SkyRideMode = 'rider' | 'driver'
|
||||
|
||||
export type SkyRideServiceClass = 'taxi' | 'comfort' | 'xl' | 'premium'
|
||||
|
||||
export type SkyRideDistanceUnit = 'kilometer' | 'mile'
|
||||
|
||||
export type SkyRideFareMode = 'calculated' | 'custom'
|
||||
|
||||
export type SkyRideRideStatus =
|
||||
| 'searching'
|
||||
| 'accepted'
|
||||
| 'driver_arriving'
|
||||
| 'arrived'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'cancelled'
|
||||
|
||||
export type SkyRideCoordinates = {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export type SkyRideLocation = {
|
||||
coords: SkyRideCoordinates
|
||||
id?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type SkyRidePerson = {
|
||||
avatarUrl: string | null
|
||||
id: string
|
||||
name: string
|
||||
phoneNumber?: string
|
||||
rating: number
|
||||
trips: number
|
||||
}
|
||||
|
||||
export type SkyRideVehicle = {
|
||||
color: string
|
||||
model: string
|
||||
plate: string
|
||||
}
|
||||
|
||||
export type SkyRideDriver = SkyRidePerson & {
|
||||
location?: SkyRideCoordinates
|
||||
vehicle: SkyRideVehicle
|
||||
}
|
||||
|
||||
export type SkyRidePassenger = Pick<
|
||||
SkyRidePerson,
|
||||
'avatarUrl' | 'id' | 'name' | 'phoneNumber' | 'rating' | 'trips'
|
||||
>
|
||||
|
||||
export type SkyRideQuoteOption = {
|
||||
available: boolean
|
||||
calculatedPrice: number
|
||||
currency: string
|
||||
etaMinutes: number
|
||||
fareMode: SkyRideFareMode
|
||||
maximumCustomPrice: number
|
||||
minimumCustomPrice: number
|
||||
price: number
|
||||
pricePerDistanceUnit: number
|
||||
quoteId: string
|
||||
seats: number
|
||||
serviceClass: SkyRideServiceClass
|
||||
}
|
||||
|
||||
export type SkyRideCustomFareInput = {
|
||||
price: number
|
||||
serviceClass: SkyRideServiceClass
|
||||
}
|
||||
|
||||
export type SkyRideQuote = {
|
||||
destination: SkyRideLocation
|
||||
distance: number
|
||||
distanceMeters: number
|
||||
distanceUnit: SkyRideDistanceUnit
|
||||
durationSeconds: number
|
||||
expiresAt: number
|
||||
options: SkyRideQuoteOption[]
|
||||
pickup: SkyRideLocation
|
||||
}
|
||||
|
||||
export type SkyRideRide = {
|
||||
createdAt: number
|
||||
currency: string
|
||||
destination: SkyRideLocation
|
||||
driver: SkyRideDriver | null
|
||||
finalPrice?: number
|
||||
id: string
|
||||
passenger?: SkyRidePassenger
|
||||
pickup: SkyRideLocation
|
||||
price: number
|
||||
serviceClass: SkyRideServiceClass
|
||||
status: SkyRideRideStatus
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export type SkyRideProfile = {
|
||||
acceptanceRate: number | null
|
||||
avatarUrl: string | null
|
||||
cancelledRides: number
|
||||
completedRides: number
|
||||
currency: string
|
||||
defaultPaymentMethod: string
|
||||
earningsToday: number | null
|
||||
id: string
|
||||
memberSince: number
|
||||
name: string
|
||||
rating: number
|
||||
}
|
||||
|
||||
export type SkyRideBootstrap = {
|
||||
activeRide: SkyRideRide | null
|
||||
availableRequests: SkyRideRide[]
|
||||
driverEligible: boolean
|
||||
driverOnline: boolean
|
||||
history: SkyRideRide[]
|
||||
pendingRating: SkyRideRide | null
|
||||
profile: SkyRideProfile
|
||||
quickLocations: SkyRideLocation[]
|
||||
}
|
||||
|
||||
export type SkyRideStateUpdate = Partial<
|
||||
Pick<
|
||||
SkyRideBootstrap,
|
||||
| 'activeRide'
|
||||
| 'availableRequests'
|
||||
| 'driverOnline'
|
||||
| 'history'
|
||||
| 'pendingRating'
|
||||
| 'profile'
|
||||
>
|
||||
>
|
||||
|
||||
export type SkyRideHistoryResponse = {
|
||||
items: SkyRideRide[]
|
||||
}
|
||||
|
||||
export type SkyRideChangedMessage = {
|
||||
data: SkyRideStateUpdate
|
||||
type: 'skyride:changed'
|
||||
}
|
||||
@@ -89,6 +89,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
|
||||
weather: { enabled: true, sounds: true },
|
||||
banking: { enabled: true, sounds: true },
|
||||
garage: { enabled: true, sounds: true },
|
||||
skyride: { enabled: true, sounds: true },
|
||||
mail: { enabled: true, sounds: true },
|
||||
map: { enabled: true, sounds: true },
|
||||
notes: { enabled: true, sounds: true },
|
||||
|
||||
@@ -89,7 +89,7 @@ const darkMessagebarColors = {
|
||||
toolbarIconIos: 'fill-[#0a84ff]',
|
||||
}
|
||||
const darkSearchbarColors = {
|
||||
inputBgIos: 'bg-[#1c1c1e]',
|
||||
inputBgIos: 'bg-transparent',
|
||||
placeholderIos: 'placeholder-[#8e8e93]',
|
||||
}
|
||||
const darkSheetColors = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,10 @@ function isoTime(offsetMilliseconds) {
|
||||
return new Date(Date.now() + offsetMilliseconds).toISOString()
|
||||
}
|
||||
|
||||
function unixTime(offsetSeconds = 0) {
|
||||
return Math.floor(Date.now() / 1000) + offsetSeconds
|
||||
}
|
||||
|
||||
let authenticated = true
|
||||
let draft = null
|
||||
const radioData = {
|
||||
@@ -464,6 +468,155 @@ const mockGarageVehicles = [
|
||||
|
||||
let mockGarageValet = null
|
||||
|
||||
const skyRideProfile = {
|
||||
acceptanceRate: 96,
|
||||
avatarUrl:
|
||||
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=240&q=80',
|
||||
cancelledRides: 3,
|
||||
completedRides: 146,
|
||||
currency: '$',
|
||||
defaultPaymentMethod: 'bank',
|
||||
earningsToday: 284,
|
||||
id: 'skyride-profile-demo',
|
||||
memberSince: unixTime(-214 * 24 * 60 * 60),
|
||||
name: 'Alex Morgan',
|
||||
rating: 4.92,
|
||||
}
|
||||
const skyRideDriver = {
|
||||
avatarUrl:
|
||||
'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&w=240&q=80',
|
||||
id: 'skyride-driver-nova',
|
||||
location: { x: -121.8, y: -904.6, z: 29.3 },
|
||||
name: 'Nova Hayes',
|
||||
phoneNumber: '5550199',
|
||||
rating: 4.97,
|
||||
trips: 842,
|
||||
vehicle: {
|
||||
color: 'Obsidian Black',
|
||||
model: 'Enus Deity',
|
||||
plate: 'RIDE 01',
|
||||
},
|
||||
}
|
||||
const skyRidePassenger = {
|
||||
avatarUrl: skyRideProfile.avatarUrl,
|
||||
id: skyRideProfile.id,
|
||||
name: skyRideProfile.name,
|
||||
phoneNumber: '5550142',
|
||||
rating: skyRideProfile.rating,
|
||||
trips: skyRideProfile.completedRides,
|
||||
}
|
||||
|
||||
function skyRideWithoutLiveContact(ride) {
|
||||
const passenger = { ...ride.passenger }
|
||||
delete passenger.phoneNumber
|
||||
let driver = null
|
||||
if (ride.driver) {
|
||||
driver = { ...ride.driver }
|
||||
delete driver.location
|
||||
delete driver.phoneNumber
|
||||
}
|
||||
return { ...ride, driver, passenger }
|
||||
}
|
||||
|
||||
const skyRideQuickLocations = [
|
||||
{
|
||||
coords: { x: -265.1, y: -960.2, z: 31.2 },
|
||||
id: 'legion-square',
|
||||
label: 'Legion Square',
|
||||
},
|
||||
{
|
||||
coords: { x: 925.2, y: 46.4, z: 81.1 },
|
||||
id: 'diamond-casino',
|
||||
label: 'Diamond Casino',
|
||||
},
|
||||
{
|
||||
coords: { x: -1037.7, y: -2737.8, z: 20.2 },
|
||||
id: 'airport',
|
||||
label: 'Los Santos Airport',
|
||||
},
|
||||
{
|
||||
coords: { x: -594.4, y: -929.9, z: 23.9 },
|
||||
id: 'vinewood',
|
||||
label: 'Vinewood',
|
||||
},
|
||||
]
|
||||
let skyRideSequence = 4
|
||||
let skyRideDriverOnline = false
|
||||
let skyRideActiveRide = null
|
||||
let skyRidePendingRating = null
|
||||
let skyRideAvailableRequests = [
|
||||
{
|
||||
createdAt: unixTime(-90),
|
||||
currency: '$',
|
||||
destination: skyRideQuickLocations[1],
|
||||
driver: null,
|
||||
id: 'skyride-request-demo',
|
||||
passenger: {
|
||||
avatarUrl:
|
||||
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=240&q=80',
|
||||
id: 'skyride-passenger-jordan',
|
||||
name: 'Jordan Lee',
|
||||
rating: 4.84,
|
||||
trips: 38,
|
||||
},
|
||||
pickup: skyRideQuickLocations[0],
|
||||
price: 42,
|
||||
serviceClass: 'comfort',
|
||||
status: 'searching',
|
||||
updatedAt: unixTime(-90),
|
||||
},
|
||||
]
|
||||
let skyRideHistory = [
|
||||
skyRideWithoutLiveContact({
|
||||
createdAt: unixTime(-22 * 60 * 60),
|
||||
currency: '$',
|
||||
destination: skyRideQuickLocations[3],
|
||||
driver: skyRideDriver,
|
||||
finalPrice: 31,
|
||||
id: 'skyride-history-1',
|
||||
passenger: skyRidePassenger,
|
||||
pickup: skyRideQuickLocations[0],
|
||||
price: 31,
|
||||
serviceClass: 'taxi',
|
||||
status: 'completed',
|
||||
updatedAt: unixTime(-21 * 60 * 60),
|
||||
}),
|
||||
skyRideWithoutLiveContact({
|
||||
createdAt: unixTime(-3 * 24 * 60 * 60),
|
||||
currency: '$',
|
||||
destination: skyRideQuickLocations[2],
|
||||
driver: skyRideDriver,
|
||||
finalPrice: 68,
|
||||
id: 'skyride-history-2',
|
||||
passenger: skyRidePassenger,
|
||||
pickup: skyRideQuickLocations[1],
|
||||
price: 68,
|
||||
serviceClass: 'premium',
|
||||
status: 'completed',
|
||||
updatedAt: unixTime(-3 * 24 * 60 * 60 + 18 * 60),
|
||||
}),
|
||||
]
|
||||
const skyRideQuotes = new Map()
|
||||
|
||||
function skyRideBootstrap() {
|
||||
return {
|
||||
activeRide: skyRideActiveRide,
|
||||
availableRequests: skyRideAvailableRequests,
|
||||
driverEligible: true,
|
||||
driverOnline: skyRideDriverOnline,
|
||||
history: skyRideHistory,
|
||||
pendingRating: skyRidePendingRating,
|
||||
profile: skyRideProfile,
|
||||
quickLocations: skyRideQuickLocations,
|
||||
}
|
||||
}
|
||||
|
||||
function skyRideUpdate(fields) {
|
||||
return Object.fromEntries(
|
||||
fields.map((field) => [field, skyRideBootstrap()[field]]),
|
||||
)
|
||||
}
|
||||
|
||||
let contactSequence = 2
|
||||
const contacts = [
|
||||
{
|
||||
@@ -2309,6 +2462,377 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:get-player-coords') {
|
||||
response.json({
|
||||
success: true,
|
||||
data: { coords: { x: -265.1, y: -960.2, z: 31.2 } },
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:bootstrap') {
|
||||
response.json({ success: true, data: skyRideBootstrap() })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:history') {
|
||||
response.json({ success: true, data: { items: skyRideHistory } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:quote') {
|
||||
const pickup = request.body.pickup
|
||||
const destination = request.body.destination
|
||||
if (!pickup?.coords || !destination?.coords) {
|
||||
response.json({ success: false, error: 'invalid_location' })
|
||||
return
|
||||
}
|
||||
|
||||
const distanceMeters = Math.max(
|
||||
750,
|
||||
Math.round(
|
||||
Math.hypot(
|
||||
Number(destination.coords.x) - Number(pickup.coords.x),
|
||||
Number(destination.coords.y) - Number(pickup.coords.y),
|
||||
) * 1.25,
|
||||
),
|
||||
)
|
||||
const durationSeconds = Math.max(60, Math.round(distanceMeters / 12))
|
||||
const distanceUnit = 'kilometer'
|
||||
const distance = distanceMeters / 1000
|
||||
const expiresAt = unixTime(2 * 60)
|
||||
const definitions = [
|
||||
{
|
||||
baseFare: 12,
|
||||
etaMinutes: 3,
|
||||
minimumFare: 15,
|
||||
pricePerKilometer: 18,
|
||||
pricePerMile: 29,
|
||||
pricePerMinute: 1,
|
||||
seats: 4,
|
||||
serviceClass: 'taxi',
|
||||
},
|
||||
{
|
||||
baseFare: 16,
|
||||
etaMinutes: 5,
|
||||
minimumFare: 22,
|
||||
pricePerKilometer: 24,
|
||||
pricePerMile: 39,
|
||||
pricePerMinute: 2,
|
||||
seats: 4,
|
||||
serviceClass: 'comfort',
|
||||
},
|
||||
{
|
||||
baseFare: 20,
|
||||
etaMinutes: 7,
|
||||
minimumFare: 28,
|
||||
pricePerKilometer: 30,
|
||||
pricePerMile: 48,
|
||||
pricePerMinute: 2,
|
||||
seats: 6,
|
||||
serviceClass: 'xl',
|
||||
},
|
||||
{
|
||||
baseFare: 28,
|
||||
etaMinutes: 8,
|
||||
minimumFare: 38,
|
||||
pricePerKilometer: 40,
|
||||
pricePerMile: 64,
|
||||
pricePerMinute: 3,
|
||||
seats: 4,
|
||||
serviceClass: 'premium',
|
||||
},
|
||||
]
|
||||
const requestedCustomFare = request.body.customFare
|
||||
if (
|
||||
requestedCustomFare !== undefined &&
|
||||
(!definitions.some(
|
||||
(definition) =>
|
||||
definition.serviceClass === requestedCustomFare?.serviceClass,
|
||||
) ||
|
||||
!Number.isInteger(requestedCustomFare?.price))
|
||||
) {
|
||||
response.json({ success: false, error: 'invalid_custom_fare' })
|
||||
return
|
||||
}
|
||||
const options = definitions.map((definition) => {
|
||||
const pricePerDistanceUnit =
|
||||
distanceUnit === 'mile'
|
||||
? definition.pricePerMile
|
||||
: definition.pricePerKilometer
|
||||
const calculatedPrice = Math.round(
|
||||
Math.max(
|
||||
definition.minimumFare,
|
||||
definition.baseFare +
|
||||
distance * pricePerDistanceUnit +
|
||||
(durationSeconds / 60) * definition.pricePerMinute,
|
||||
),
|
||||
)
|
||||
const minimumCustomPrice = Math.round(
|
||||
Math.max(5, definition.minimumFare, calculatedPrice * 0.5),
|
||||
)
|
||||
const maximumCustomPrice = Math.round(
|
||||
Math.min(100000, calculatedPrice * 3),
|
||||
)
|
||||
const usesCustomFare =
|
||||
requestedCustomFare?.serviceClass === definition.serviceClass
|
||||
const requestedPrice = Number(requestedCustomFare?.price)
|
||||
if (
|
||||
usesCustomFare &&
|
||||
(!Number.isInteger(requestedPrice) ||
|
||||
requestedPrice < minimumCustomPrice ||
|
||||
requestedPrice > maximumCustomPrice)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const option = {
|
||||
available: true,
|
||||
calculatedPrice,
|
||||
currency: '$',
|
||||
etaMinutes: definition.etaMinutes,
|
||||
fareMode: usesCustomFare ? 'custom' : 'calculated',
|
||||
maximumCustomPrice,
|
||||
minimumCustomPrice,
|
||||
price: usesCustomFare ? requestedPrice : calculatedPrice,
|
||||
pricePerDistanceUnit,
|
||||
quoteId: 'skyride-quote-' + Date.now() + '-' + definition.serviceClass,
|
||||
seats: definition.seats,
|
||||
serviceClass: definition.serviceClass,
|
||||
}
|
||||
skyRideQuotes.set(option.quoteId, {
|
||||
destination,
|
||||
expiresAt,
|
||||
option,
|
||||
pickup,
|
||||
})
|
||||
return option
|
||||
})
|
||||
if (options.some((option) => option === null)) {
|
||||
response.json({ success: false, error: 'invalid_custom_fare' })
|
||||
return
|
||||
}
|
||||
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
destination,
|
||||
distance,
|
||||
distanceMeters,
|
||||
distanceUnit,
|
||||
durationSeconds,
|
||||
expiresAt,
|
||||
options,
|
||||
pickup,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:request') {
|
||||
const quote = skyRideQuotes.get(String(request.body.quoteId ?? ''))
|
||||
if (!quote) {
|
||||
response.json({ success: false, error: 'quote_not_found' })
|
||||
return
|
||||
}
|
||||
if (quote.expiresAt <= unixTime()) {
|
||||
response.json({ success: false, error: 'quote_expired' })
|
||||
return
|
||||
}
|
||||
if (skyRideActiveRide) {
|
||||
response.json({ success: false, error: 'active_ride_exists' })
|
||||
return
|
||||
}
|
||||
|
||||
const now = unixTime()
|
||||
const ride = {
|
||||
createdAt: now,
|
||||
currency: quote.option.currency,
|
||||
destination: quote.destination,
|
||||
driver: null,
|
||||
id: `skyride-ride-${skyRideSequence++}`,
|
||||
passenger: skyRidePassenger,
|
||||
pickup: quote.pickup,
|
||||
price: quote.option.price,
|
||||
serviceClass: quote.option.serviceClass,
|
||||
status: 'searching',
|
||||
updatedAt: now,
|
||||
}
|
||||
skyRideActiveRide = ride
|
||||
skyRideAvailableRequests = [
|
||||
skyRideWithoutLiveContact(ride),
|
||||
...skyRideAvailableRequests,
|
||||
]
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['activeRide']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:set-driver-status') {
|
||||
if (typeof request.body.online !== 'boolean') {
|
||||
response.json({ success: false, error: 'invalid_driver_status' })
|
||||
return
|
||||
}
|
||||
skyRideDriverOnline = request.body.online
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['driverOnline', 'availableRequests']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:accept') {
|
||||
if (!skyRideDriverOnline) {
|
||||
response.json({ success: false, error: 'driver_offline' })
|
||||
return
|
||||
}
|
||||
const ride = skyRideAvailableRequests.find(
|
||||
(item) => item.id === request.body.rideId,
|
||||
)
|
||||
if (!ride) {
|
||||
response.json({ success: false, error: 'ride_not_found' })
|
||||
return
|
||||
}
|
||||
if (skyRideActiveRide && skyRideActiveRide.id !== ride.id) {
|
||||
response.json({ success: false, error: 'active_ride_exists' })
|
||||
return
|
||||
}
|
||||
|
||||
skyRideActiveRide = {
|
||||
...ride,
|
||||
driver: skyRideDriver,
|
||||
status: 'accepted',
|
||||
updatedAt: unixTime(),
|
||||
}
|
||||
skyRideAvailableRequests = skyRideAvailableRequests.filter(
|
||||
(item) => item.id !== ride.id,
|
||||
)
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['activeRide', 'availableRequests']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (
|
||||
endpoint === 'skyride:arrive' ||
|
||||
endpoint === 'skyride:start' ||
|
||||
endpoint === 'skyride:complete'
|
||||
) {
|
||||
if (!skyRideActiveRide || skyRideActiveRide.id !== request.body.rideId) {
|
||||
response.json({ success: false, error: 'ride_not_found' })
|
||||
return
|
||||
}
|
||||
|
||||
const transition = {
|
||||
'skyride:arrive': {
|
||||
from: ['accepted', 'driver_arriving'],
|
||||
to: 'arrived',
|
||||
},
|
||||
'skyride:start': { from: ['arrived'], to: 'in_progress' },
|
||||
'skyride:complete': { from: ['in_progress'], to: 'completed' },
|
||||
}[endpoint]
|
||||
if (!transition.from.includes(skyRideActiveRide.status)) {
|
||||
response.json({ success: false, error: 'invalid_ride_status' })
|
||||
return
|
||||
}
|
||||
|
||||
const updatedRide = {
|
||||
...skyRideActiveRide,
|
||||
...(transition.to === 'completed'
|
||||
? { finalPrice: skyRideActiveRide.price }
|
||||
: {}),
|
||||
status: transition.to,
|
||||
updatedAt: unixTime(),
|
||||
}
|
||||
if (transition.to === 'completed') {
|
||||
const passengerRide = updatedRide.passenger?.id === skyRideProfile.id
|
||||
skyRideProfile.completedRides += 1
|
||||
if (!passengerRide) {
|
||||
skyRideProfile.earningsToday += Math.round(
|
||||
updatedRide.finalPrice * 0.85,
|
||||
)
|
||||
}
|
||||
const historyRide = skyRideWithoutLiveContact(updatedRide)
|
||||
skyRideHistory = [historyRide, ...skyRideHistory]
|
||||
skyRidePendingRating = passengerRide ? historyRide : null
|
||||
skyRideActiveRide = null
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate([
|
||||
'activeRide',
|
||||
'history',
|
||||
'pendingRating',
|
||||
'profile',
|
||||
]),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
skyRideActiveRide = updatedRide
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['activeRide']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:cancel') {
|
||||
const ride =
|
||||
skyRideActiveRide?.id === request.body.rideId
|
||||
? skyRideActiveRide
|
||||
: skyRideAvailableRequests.find(
|
||||
(item) => item.id === request.body.rideId,
|
||||
)
|
||||
if (!ride) {
|
||||
response.json({ success: false, error: 'ride_not_found' })
|
||||
return
|
||||
}
|
||||
|
||||
const cancelledRide = {
|
||||
...ride,
|
||||
status: 'cancelled',
|
||||
updatedAt: unixTime(),
|
||||
}
|
||||
skyRideAvailableRequests = skyRideAvailableRequests.filter(
|
||||
(item) => item.id !== ride.id,
|
||||
)
|
||||
if (skyRideActiveRide?.id === ride.id) skyRideActiveRide = null
|
||||
skyRidePendingRating = null
|
||||
skyRideHistory = [
|
||||
skyRideWithoutLiveContact(cancelledRide),
|
||||
...skyRideHistory,
|
||||
]
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate([
|
||||
'activeRide',
|
||||
'availableRequests',
|
||||
'history',
|
||||
'pendingRating',
|
||||
]),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skyride:rate') {
|
||||
const rating = Number(request.body.rating)
|
||||
const tip = Number(request.body.tip)
|
||||
if (
|
||||
!skyRidePendingRating ||
|
||||
skyRidePendingRating.id !== request.body.rideId
|
||||
) {
|
||||
response.json({ success: false, error: 'ride_not_found' })
|
||||
return
|
||||
}
|
||||
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
|
||||
response.json({ success: false, error: 'invalid_rating' })
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(tip) || tip < 0 || tip > 10000) {
|
||||
response.json({ success: false, error: 'invalid_tip' })
|
||||
return
|
||||
}
|
||||
|
||||
skyRidePendingRating = null
|
||||
response.json({
|
||||
success: true,
|
||||
data: skyRideUpdate(['history', 'pendingRating']),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'banking:overview') {
|
||||
response.json({ success: true, data: bankingOverview() })
|
||||
return
|
||||
|
||||
+101
-4
@@ -115,12 +115,12 @@ Config.Animations = {
|
||||
},
|
||||
Transforms = {
|
||||
Portrait = {
|
||||
position = { x = 0.0, y = 0.0, z = 0.0 },
|
||||
rotation = { x = 0.0, y = 0.0, z = 0.0 },
|
||||
position = vector3(0.0, 0.0, 0.0),
|
||||
rotation = vector3(0.0, 0.0, 0.0),
|
||||
},
|
||||
Landscape = {
|
||||
position = { x = 0.0, y = 0.0, z = 0.0 },
|
||||
rotation = { x = 0.0, y = 0.0, z = 90.0 },
|
||||
position = vector3(0.0, 0.0, 0.0),
|
||||
rotation = vector3(0.0, 0.0, 90.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -320,3 +320,100 @@ Config.Calendar = {
|
||||
FutureSeconds = 5 * 365 * 24 * 60 * 60,
|
||||
ReminderPollSeconds = 15,
|
||||
}
|
||||
|
||||
Config.SkyRide = {
|
||||
PaymentAccount = "bank",
|
||||
Currency = "$",
|
||||
DistanceUnit = "kilometer", -- kilometer or mile
|
||||
QuoteLifetimeSeconds = 120,
|
||||
HistoryLimit = 50,
|
||||
ReadsPerMinute = 120,
|
||||
ActionsPerMinute = 30,
|
||||
QuotesPerMinute = 20,
|
||||
RecoveryIntervalSeconds = 15,
|
||||
PaymentPendingRecoverySeconds = 30,
|
||||
PickupSelectionRadius = 150.0,
|
||||
ArrivalRadius = 40.0,
|
||||
CompletionRadius = 80.0,
|
||||
MinimumDistanceMeters = 250,
|
||||
MaximumDistanceMeters = 40000,
|
||||
RouteDistanceMultiplier = 1.25,
|
||||
AverageSpeedMetersPerSecond = 12.0,
|
||||
DriverPayoutPercent = 85,
|
||||
RatingCommentMaxLength = 300,
|
||||
MaximumTip = 10000,
|
||||
CustomFare = {
|
||||
Enabled = true,
|
||||
MinimumPrice = 5,
|
||||
MaximumPrice = 100000,
|
||||
MinimumCalculatedMultiplier = 0.5,
|
||||
MaximumCalculatedMultiplier = 3.0,
|
||||
},
|
||||
DriverJobs = {
|
||||
taxi = 0,
|
||||
},
|
||||
Services = {
|
||||
{
|
||||
Id = "taxi",
|
||||
Seats = 4,
|
||||
EtaMinutes = 3,
|
||||
BaseFare = 12,
|
||||
PricePerKilometer = 18,
|
||||
PricePerMile = 29,
|
||||
PricePerMinute = 1,
|
||||
MinimumFare = 15,
|
||||
},
|
||||
{
|
||||
Id = "comfort",
|
||||
Seats = 4,
|
||||
EtaMinutes = 5,
|
||||
BaseFare = 16,
|
||||
PricePerKilometer = 24,
|
||||
PricePerMile = 39,
|
||||
PricePerMinute = 2,
|
||||
MinimumFare = 22,
|
||||
},
|
||||
{
|
||||
Id = "xl",
|
||||
Seats = 6,
|
||||
EtaMinutes = 7,
|
||||
BaseFare = 20,
|
||||
PricePerKilometer = 30,
|
||||
PricePerMile = 48,
|
||||
PricePerMinute = 2,
|
||||
MinimumFare = 28,
|
||||
},
|
||||
{
|
||||
Id = "premium",
|
||||
Seats = 4,
|
||||
EtaMinutes = 8,
|
||||
BaseFare = 28,
|
||||
PricePerKilometer = 40,
|
||||
PricePerMile = 64,
|
||||
PricePerMinute = 3,
|
||||
MinimumFare = 38,
|
||||
},
|
||||
},
|
||||
QuickLocations = {
|
||||
{
|
||||
Id = "legion-square",
|
||||
Label = "Legion Square",
|
||||
Coords = vector3(-265.1, -960.2, 31.2),
|
||||
},
|
||||
{
|
||||
Id = "diamond-casino",
|
||||
Label = "Diamond Casino",
|
||||
Coords = vector3(925.2, 46.4, 81.1),
|
||||
},
|
||||
{
|
||||
Id = "airport",
|
||||
Label = "Los Santos Airport",
|
||||
Coords = vector3(-1037.7, -2737.8, 20.2),
|
||||
},
|
||||
{
|
||||
Id = "vinewood",
|
||||
Label = "Vinewood",
|
||||
Coords = vector3(-594.4, -929.9, 23.9),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -396,6 +396,95 @@ Locales["en"] = {
|
||||
request_failed = "The garage request failed.", 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.", quote_not_found = "These ride options are no longer available.",
|
||||
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_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.", not_ride_participant = "You are not part of this ride.",
|
||||
invalid_ride_status = "This action is not available for the ride.", invalid_rating = "Choose a rating from one to five stars.",
|
||||
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_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.", request_failed = "SkyRide could not complete the request.",
|
||||
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.",
|
||||
default = "SkyRide could not complete the request.",
|
||||
},
|
||||
},
|
||||
calculator = { name = "Calculator" },
|
||||
snake = {
|
||||
name = "Snake", backToMenu = "Back to game menu", board = "Snake game board",
|
||||
|
||||
@@ -27,6 +27,7 @@ client_scripts {
|
||||
'source/client/animations.lua',
|
||||
'source/client/camera.lua',
|
||||
'source/client/garage.lua',
|
||||
'source/client/skyride.lua',
|
||||
'source/bridge/client/radio.lua',
|
||||
'source/client/main.lua',
|
||||
'source/client/radio.lua',
|
||||
@@ -61,6 +62,7 @@ server_scripts {
|
||||
'source/server/fliptok.lua',
|
||||
'source/server/picstagram.lua',
|
||||
'source/server/map.lua',
|
||||
'source/server/skyride.lua',
|
||||
'source/server/calendar.lua',
|
||||
'source/server/radio.lua',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
local server_callbacks = {
|
||||
"skyride:bootstrap",
|
||||
"skyride:quote",
|
||||
"skyride:request",
|
||||
"skyride:set-driver-status",
|
||||
"skyride:accept",
|
||||
"skyride:arrive",
|
||||
"skyride:start",
|
||||
"skyride:complete",
|
||||
"skyride:cancel",
|
||||
"skyride:rate",
|
||||
"skyride:history",
|
||||
}
|
||||
|
||||
local function normalize_person(person)
|
||||
if type(person) ~= "table" then
|
||||
return
|
||||
end
|
||||
if person.avatarUrl == false then
|
||||
person.avatarUrl = json.null
|
||||
end
|
||||
end
|
||||
|
||||
local function normalize_ride(ride)
|
||||
if type(ride) ~= "table" then
|
||||
return
|
||||
end
|
||||
normalize_person(ride.passenger)
|
||||
if ride.driver == false then
|
||||
ride.driver = json.null
|
||||
return
|
||||
end
|
||||
if type(ride.driver) ~= "table" then
|
||||
return
|
||||
end
|
||||
normalize_person(ride.driver)
|
||||
local vehicle = ride.driver.vehicle
|
||||
local model_hash = type(vehicle) == "table" and tonumber(vehicle.model) or nil
|
||||
if not model_hash then
|
||||
return
|
||||
end
|
||||
local display_name = GetDisplayNameFromVehicleModel(model_hash)
|
||||
local label = display_name and GetLabelText(display_name) or nil
|
||||
if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
|
||||
vehicle.model = label
|
||||
end
|
||||
end
|
||||
|
||||
local function normalize_rides(rides)
|
||||
if type(rides) ~= "table" then
|
||||
return
|
||||
end
|
||||
for index = 1, #rides do
|
||||
normalize_ride(rides[index])
|
||||
end
|
||||
end
|
||||
|
||||
local function normalize_state(data)
|
||||
if type(data) ~= "table" then
|
||||
return
|
||||
end
|
||||
if data.activeRide == false then
|
||||
data.activeRide = json.null
|
||||
else
|
||||
normalize_ride(data.activeRide)
|
||||
end
|
||||
if data.pendingRating == false then
|
||||
data.pendingRating = json.null
|
||||
else
|
||||
normalize_ride(data.pendingRating)
|
||||
end
|
||||
normalize_rides(data.availableRequests)
|
||||
normalize_rides(data.history)
|
||||
normalize_rides(data.items)
|
||||
if type(data.profile) == "table" then
|
||||
if data.profile.acceptanceRate == false then
|
||||
data.profile.acceptanceRate = json.null
|
||||
end
|
||||
if data.profile.avatarUrl == false then
|
||||
data.profile.avatarUrl = json.null
|
||||
end
|
||||
if data.profile.earningsToday == false then
|
||||
data.profile.earningsToday = json.null
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for index = 1, #server_callbacks do
|
||||
local callback_name = server_callbacks[index]
|
||||
RegisterNUICallback(callback_name, function(data, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data or {})
|
||||
if not result then
|
||||
cb({ success = false, error = "request_failed" })
|
||||
return
|
||||
end
|
||||
if result.success and type(result.data) == "table" then
|
||||
normalize_state(result.data)
|
||||
end
|
||||
cb(result)
|
||||
end)
|
||||
end
|
||||
|
||||
RegisterNUICallback("skyride:get-player-coords", function(_, cb)
|
||||
local coords = GetEntityCoords(PlayerPedId())
|
||||
cb({
|
||||
success = true,
|
||||
data = {
|
||||
coords = {
|
||||
x = coords.x,
|
||||
y = coords.y,
|
||||
z = coords.z,
|
||||
},
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
RegisterNetEvent("sky_phone:skyride:changed", function(data)
|
||||
normalize_state(data)
|
||||
SendNUIMessage({ type = "skyride:changed", data = data })
|
||||
end)
|
||||
@@ -1517,6 +1517,125 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_skyride_profiles",
|
||||
columns = {
|
||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{
|
||||
name = "owner_identifier",
|
||||
type = "VARCHAR(80) NOT NULL",
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_skyride_owner", columns = "(`owner_identifier`)" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_skyride_rides",
|
||||
columns = {
|
||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{
|
||||
name = "passenger_profile_id",
|
||||
type = "CHAR(36) NOT NULL",
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{
|
||||
name = "driver_profile_id",
|
||||
type = "CHAR(36) NULL",
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{ name = "passenger_name", type = "VARCHAR(80) NOT NULL" },
|
||||
{ name = "driver_name", type = "VARCHAR(80) NULL" },
|
||||
{
|
||||
name = "status",
|
||||
type = "ENUM('payment_pending','searching','accepted','arrived','in_progress','completing','completed','cancelled') NOT NULL",
|
||||
},
|
||||
{
|
||||
name = "service_class",
|
||||
type = "ENUM('taxi','comfort','xl','premium') NOT NULL",
|
||||
},
|
||||
{ name = "pickup_label", type = "VARCHAR(80) NOT NULL" },
|
||||
{ name = "pickup_x", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "pickup_y", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "pickup_z", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "destination_label", type = "VARCHAR(80) NOT NULL" },
|
||||
{ name = "destination_x", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "destination_y", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "destination_z", type = "DECIMAL(10,3) NOT NULL" },
|
||||
{ name = "distance_meters", type = "INT UNSIGNED NOT NULL" },
|
||||
{ name = "duration_seconds", type = "INT UNSIGNED NOT NULL" },
|
||||
{ name = "price", type = "INT UNSIGNED NOT NULL" },
|
||||
{ name = "payout_amount", type = "INT UNSIGNED NOT NULL" },
|
||||
{
|
||||
name = "currency",
|
||||
type = "VARCHAR(8) NOT NULL",
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_general_ci",
|
||||
},
|
||||
{ name = "driver_vehicle_model", type = "VARCHAR(64) NULL" },
|
||||
{ name = "driver_vehicle_color", type = "VARCHAR(64) NULL" },
|
||||
{ name = "driver_vehicle_plate", type = "VARCHAR(16) NULL" },
|
||||
{ name = "cancelled_by", type = "ENUM('passenger','driver','system') NULL" },
|
||||
{ name = "cancel_reason", type = "VARCHAR(32) NULL" },
|
||||
{ name = "passenger_rating", type = "TINYINT UNSIGNED NULL" },
|
||||
{ name = "rating_comment", type = "VARCHAR(300) NULL" },
|
||||
{ name = "tip_amount", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
{
|
||||
name = "refund_status",
|
||||
type = "ENUM('none','pending','processing','completed') NOT NULL DEFAULT 'none'",
|
||||
},
|
||||
{
|
||||
name = "tip_status",
|
||||
type = "ENUM('none','processing','completed','failed') NOT NULL DEFAULT 'none'",
|
||||
},
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{
|
||||
name = "updated_at",
|
||||
type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
|
||||
},
|
||||
{ name = "accepted_at", type = "DATETIME NULL" },
|
||||
{ name = "arrived_at", type = "DATETIME NULL" },
|
||||
{ name = "started_at", type = "DATETIME NULL" },
|
||||
{ name = "completed_at", type = "DATETIME NULL" },
|
||||
{ name = "cancelled_at", type = "DATETIME NULL" },
|
||||
{ name = "refunded_at", type = "DATETIME NULL" },
|
||||
{ name = "paid_out_at", type = "DATETIME NULL" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
indexes = {
|
||||
{
|
||||
name = "idx_sky_phone_skyride_passenger",
|
||||
columns = "(`passenger_profile_id`, `status`, `updated_at`)",
|
||||
},
|
||||
{
|
||||
name = "idx_sky_phone_skyride_driver",
|
||||
columns = "(`driver_profile_id`, `status`, `updated_at`)",
|
||||
},
|
||||
{ name = "idx_sky_phone_skyride_requests", columns = "(`status`, `created_at`)" },
|
||||
{
|
||||
name = "idx_sky_phone_skyride_refunds",
|
||||
columns = "(`refund_status`, `status`, `updated_at`)",
|
||||
},
|
||||
},
|
||||
foreignKeys = {
|
||||
{
|
||||
column = "passenger_profile_id",
|
||||
references = "`sky_phone_skyride_profiles` (`id`) ON DELETE CASCADE",
|
||||
},
|
||||
{
|
||||
column = "driver_profile_id",
|
||||
references = "`sky_phone_skyride_profiles` (`id`) ON DELETE SET NULL",
|
||||
},
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
}
|
||||
|
||||
Bridge.Database.Migrate("sky_phone", schema)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -614,3 +614,60 @@ CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_moderation_audit` (
|
||||
KEY `idx_sky_phone_picstagram_audit_target` (`target_type`,`target_id`,`created_at`),
|
||||
FOREIGN KEY (`report_id`) REFERENCES `sky_phone_picstagram_reports` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_profiles` (
|
||||
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_sky_phone_skyride_owner` (`owner_identifier`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_rides` (
|
||||
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`passenger_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`driver_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
`passenger_name` VARCHAR(80) NOT NULL,
|
||||
`driver_name` VARCHAR(80) NULL,
|
||||
`status` ENUM('payment_pending','searching','accepted','arrived','in_progress','completing','completed','cancelled') NOT NULL,
|
||||
`service_class` ENUM('taxi','comfort','xl','premium') NOT NULL,
|
||||
`pickup_label` VARCHAR(80) NOT NULL,
|
||||
`pickup_x` DECIMAL(10,3) NOT NULL,
|
||||
`pickup_y` DECIMAL(10,3) NOT NULL,
|
||||
`pickup_z` DECIMAL(10,3) NOT NULL,
|
||||
`destination_label` VARCHAR(80) NOT NULL,
|
||||
`destination_x` DECIMAL(10,3) NOT NULL,
|
||||
`destination_y` DECIMAL(10,3) NOT NULL,
|
||||
`destination_z` DECIMAL(10,3) NOT NULL,
|
||||
`distance_meters` INT UNSIGNED NOT NULL,
|
||||
`duration_seconds` INT UNSIGNED NOT NULL,
|
||||
`price` INT UNSIGNED NOT NULL,
|
||||
`payout_amount` INT UNSIGNED NOT NULL,
|
||||
`currency` VARCHAR(8) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
|
||||
`driver_vehicle_model` VARCHAR(64) NULL,
|
||||
`driver_vehicle_color` VARCHAR(64) NULL,
|
||||
`driver_vehicle_plate` VARCHAR(16) NULL,
|
||||
`cancelled_by` ENUM('passenger','driver','system') NULL,
|
||||
`cancel_reason` VARCHAR(32) NULL,
|
||||
`passenger_rating` TINYINT UNSIGNED NULL,
|
||||
`rating_comment` VARCHAR(300) NULL,
|
||||
`tip_amount` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`refund_status` ENUM('none','pending','processing','completed') NOT NULL DEFAULT 'none',
|
||||
`tip_status` ENUM('none','processing','completed','failed') NOT NULL DEFAULT 'none',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`accepted_at` DATETIME NULL,
|
||||
`arrived_at` DATETIME NULL,
|
||||
`started_at` DATETIME NULL,
|
||||
`completed_at` DATETIME NULL,
|
||||
`cancelled_at` DATETIME NULL,
|
||||
`refunded_at` DATETIME NULL,
|
||||
`paid_out_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_sky_phone_skyride_passenger` (`passenger_profile_id`,`status`,`updated_at`),
|
||||
KEY `idx_sky_phone_skyride_driver` (`driver_profile_id`,`status`,`updated_at`),
|
||||
KEY `idx_sky_phone_skyride_requests` (`status`,`created_at`),
|
||||
KEY `idx_sky_phone_skyride_refunds` (`refund_status`,`status`,`updated_at`),
|
||||
FOREIGN KEY (`passenger_profile_id`) REFERENCES `sky_phone_skyride_profiles` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`driver_profile_id`) REFERENCES `sky_phone_skyride_profiles` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
Reference in New Issue
Block a user