diff --git a/package.json b/package.json
index 8aac3e8..5abdf69 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
+ "test": "vitest",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
@@ -16,9 +17,11 @@
},
"devDependencies": {
"@fontsource/poppins": "^5.0.8",
+ "@playwright/test": "^1.40.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/forms": "^0.5.7",
+ "@types/node": "^20.11.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"autoprefixer": "^10.4.16",
@@ -30,7 +33,8 @@
"tailwindcss": "^3.3.6",
"tslib": "^2.4.1",
"typescript": "^5.3.3",
- "vite": "^4.4.2"
+ "vite": "^4.4.2",
+ "vitest": "^1.2.2"
},
"type": "module",
"dependencies": {
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index e6a22bc..5ec405b 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,6 +1,6 @@
import { get } from 'svelte/store';
import { getTokensLogin, getTokensRefresh, getUserInfo } from './emel-api/emel-api';
-import { token, user, userCredentials } from './stores';
+import { token, user, userCredentials } from './state';
export async function login(email: string, password: string) {
const response = await getTokensLogin(email, password);
diff --git a/src/lib/components/Bike.svelte b/src/lib/components/Bike.svelte
index e4162b5..ef3b475 100644
--- a/src/lib/components/Bike.svelte
+++ b/src/lib/components/Bike.svelte
@@ -10,12 +10,14 @@
import IconSettings from '@tabler/icons-svelte/dist/svelte/icons/IconSettings.svelte';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
- import { reserveBike, startTrip, updateActiveTripInfo, type ThrownError } from '../gira-api';
- import { accountInfo, addErrorMessage, appSettings, currentTrip, type StationInfo } from '$lib/stores';
+ import type { ThrownError } from '../gira-api/api-types';
+ import { accountInfo, addErrorMessage, appSettings, currentTrip, type StationInfo } from '$lib/state';
import { currentPos } from '$lib/location';
import { fade } from 'svelte/transition';
import { distanceBetweenCoords } from '$lib/utils';
import { LOCK_DISTANCE_m } from '$lib/constants';
+ import { updateActiveTripInfo } from '$lib/state/helper';
+ import { reserveBike, startTrip } from '$lib/gira-api/api';
async function checkTripStarted() {
if ($currentTrip === null) return;
diff --git a/src/lib/components/ErrorMessage.svelte b/src/lib/components/ErrorMessage.svelte
index 0f6abd7..a733d99 100644
--- a/src/lib/components/ErrorMessage.svelte
+++ b/src/lib/components/ErrorMessage.svelte
@@ -1,5 +1,5 @@
diff --git a/src/lib/components/Floating.svelte b/src/lib/components/Floating.svelte
index 3437da4..00d5bbe 100644
--- a/src/lib/components/Floating.svelte
+++ b/src/lib/components/Floating.svelte
@@ -1,5 +1,5 @@
diff --git a/src/lib/components/Profile.svelte b/src/lib/components/Profile.svelte
index aa9adb7..6057d3b 100644
--- a/src/lib/components/Profile.svelte
+++ b/src/lib/components/Profile.svelte
@@ -1,7 +1,4 @@
{#if $user}
diff --git a/src/lib/components/StationMenu.svelte b/src/lib/components/StationMenu.svelte
index e768724..f3cf994 100644
--- a/src/lib/components/StationMenu.svelte
+++ b/src/lib/components/StationMenu.svelte
@@ -2,14 +2,14 @@
import { tweened } from 'svelte/motion';
import Bike from '$lib/components/Bike.svelte';
import { cubicOut } from 'svelte/easing';
- import { getStationInfo } from '$lib/gira-api';
import { onMount } from 'svelte';
- import { stations, selectedStation, type StationInfo } from '$lib/stores';
+ import { stations, selectedStation, type StationInfo } from '$lib/state';
import { tick } from 'svelte';
import { currentPos } from '$lib/location';
import { distanceBetweenCoords, formatDistance } from '$lib/utils';
import { fade } from 'svelte/transition';
import BikeSkeleton from './BikeSkeleton.svelte';
+ import { getStationInfo } from '$lib/gira-api/api';
export let bikeListHeight = 0;
export let posTop:number|undefined = 0;
diff --git a/src/lib/components/TripRating.svelte b/src/lib/components/TripRating.svelte
index d3d0a33..4d888e0 100644
--- a/src/lib/components/TripRating.svelte
+++ b/src/lib/components/TripRating.svelte
@@ -1,5 +1,5 @@
diff --git a/src/lib/emel-api/emel-api.ts b/src/lib/emel-api/emel-api.ts
index 91558bf..9ba2967 100644
--- a/src/lib/emel-api/emel-api.ts
+++ b/src/lib/emel-api/emel-api.ts
@@ -1,4 +1,4 @@
-import type { Token } from '$lib/stores';
+import type { Token } from '$lib/state';
import type { ApiResponse, TokenOpt, UserInfo } from './types';
export async function getTokensLogin(email: string, password: string) {
diff --git a/src/lib/gira-api/__mocks__/api.ts b/src/lib/gira-api/__mocks__/api.ts
new file mode 100644
index 0000000..7dfbd84
--- /dev/null
+++ b/src/lib/gira-api/__mocks__/api.ts
@@ -0,0 +1,159 @@
+import type { M, Q } from '../api-types';
+
+export async function reserveBike(serialNumber: string): Promise> {
+ return {
+ reserveBike: true,
+ };
+}
+
+export async function getStationInfo(stationId: string): Promise> {
+ // TODO
+ return { getBikes: [], getDocks: [] };
+}
+
+export async function cancelBikeReserve(): Promise> {
+ return { cancelBikeReserve: true };
+}
+
+export async function startTrip(): Promise> {
+ return {
+ startTrip: true,
+ };
+}
+
+export async function rateTrip(tripCode: string, tripRating: number, tripComment?: string, tripAttachment?: File): Promise> {
+ return {
+ rateTrip: true,
+ };
+}
+
+export async function tripPayWithNoPoints(tripCode: string): Promise> {
+ // TODO check what this actually returns?
+ return {
+ tripPayWithNoPoints: 123,
+ };
+}
+
+export async function tripPayWithPoints(tripCode: string): Promise> {
+ // TODO check what this actually returns?
+ return {
+ tripPayWithPoints: 123,
+ };
+}
+
+export async function fullOnetimeInfo(): Promise> {
+ return {
+ activeTrip: null,
+ client: [{ balance: 4, bonus: 12345 }],
+ getStations: [{
+ 'code': '0000000003',
+ 'description': 'Alameda dos Oceanos',
+ 'latitude': 38.756161,
+ 'longitude': -9.096804,
+ 'name': '101 - Alameda dos Oceanos / Rua dos Argonautas',
+ 'bikes': 5,
+ 'docks': 14,
+ 'serialNumber': '1000101',
+ 'assetStatus': 'active',
+ },
+ {
+ 'code': '0000000005',
+ 'description': 'Rua do Fogo de Santelmo',
+ 'latitude': 38.761218,
+ 'longitude': -9.095019,
+ 'name': '103 - Jardim da Água',
+ 'bikes': 2,
+ 'docks': 17,
+ 'serialNumber': '1000103',
+ 'assetStatus': 'active',
+ }],
+ activeUserSubscriptions: [
+ {
+ 'expirationDate': '2024-10-01T00:00:18Z',
+ 'subscriptionStatus': 'paid',
+ 'name': 'Passe Anual',
+ 'type': 'anual',
+ 'active': true,
+ },
+ ],
+ unratedTrips: [
+ {
+ 'code': 'JWA8FQ1PFL',
+ 'startDate': '2024-01-01T13:45:50Z',
+ 'endDate': '2024-01-01T14:01:56Z',
+ 'rating': null,
+ 'startLocation': '0000009904',
+ 'endLocation': '0000009905',
+ 'cost': 0,
+ 'costBonus': 0,
+ 'asset': '0000013066',
+ },
+ ],
+ tripHistory: (await getTripHistory(0, 10)).tripHistory,
+ };
+}
+
+export async function getTripHistory(pageNum: number, pageSize: number): Promise> {
+ return {
+ tripHistory: [
+ {
+ 'code': '6JWFVI9O8N',
+ 'startDate': '2024-01-18T20:32:38Z',
+ 'endDate': '2024-01-18T20:51:52Z',
+ 'rating': 5,
+ 'bikeName': 'E0593',
+ 'startLocation': '419 - Av. António José de Almeida / Instituto Superior Técnico',
+ 'endLocation': '484 - Rua Professor Vieira de Almeida',
+ 'bonus': 10,
+ 'usedPoints': 0,
+ 'cost': 0,
+ 'bikeType': 'electric',
+ },
+ {
+ 'code': 'JWA8FQ1PFL',
+ 'startDate': '2024-01-18T13:45:50Z',
+ 'endDate': '2024-01-18T14:01:56Z',
+ 'rating': 0,
+ 'bikeName': 'E0967',
+ 'startLocation': '484 - Rua Professor Vieira de Almeida',
+ 'endLocation': '420 - Av. Rovisco Pais / Av. Manuel da Maia',
+ 'bonus': 110,
+ 'usedPoints': 0,
+ 'cost': 0,
+ 'bikeType': 'electric',
+ },
+ {
+ 'code': '5BRNS2SR5A',
+ 'startDate': '2024-01-17T23:23:02Z',
+ 'endDate': '2024-01-17T23:32:21Z',
+ 'rating': 5,
+ 'bikeName': 'E0154',
+ 'startLocation': '476 - Av. Professor Gama Pinto / Reitoria',
+ 'endLocation': '484 - Rua Professor Vieira de Almeida',
+ 'bonus': 10,
+ 'usedPoints': 0,
+ 'cost': 0,
+ 'bikeType': 'electric',
+ },
+ {
+ 'code': 'DIIOWVFVSL',
+ 'startDate': '2024-01-17T19:28:10Z',
+ 'endDate': '2024-01-17T19:35:16Z',
+ 'rating': 5,
+ 'bikeName': 'E1953',
+ 'startLocation': '483 - Rua Professor Francisco Lucas Pires ',
+ 'endLocation': '475 - Av. Professor Gama Pinto / Cantina Velha',
+ 'bonus': 110,
+ 'usedPoints': 0,
+ 'cost': 0,
+ 'bikeType': 'electric',
+ },
+ ],
+ };
+}
+
+export async function getActiveTripInfo(): Promise> {
+ return {
+ activeTrip: null,
+ };
+}
\ No newline at end of file
diff --git a/src/lib/gira-api/types.ts b/src/lib/gira-api/api-types.ts
similarity index 99%
rename from src/lib/gira-api/types.ts
rename to src/lib/gira-api/api-types.ts
index cbf0cf1..513fc88 100644
--- a/src/lib/gira-api/types.ts
+++ b/src/lib/gira-api/api-types.ts
@@ -1,3 +1,10 @@
+export type ThrownError = {
+ errors: {message:string}[];
+ status: number;
+};
+export type Q = {[K in T[number]]:Query[K]};
+export type M = {[K in T[number]]:Mutation[K]};
+
export type Maybe = T | null
export type Exact = {
[K in keyof T]: T[K]
diff --git a/src/lib/gira-api/api.ts b/src/lib/gira-api/api.ts
new file mode 100644
index 0000000..8b2a635
--- /dev/null
+++ b/src/lib/gira-api/api.ts
@@ -0,0 +1,264 @@
+import { dev } from '$app/environment';
+import { CapacitorHttp, type HttpResponse } from '@capacitor/core';
+import { get } from 'svelte/store';
+import { Preferences } from '@capacitor/preferences';
+import type { M, Q, ThrownError } from './api-types';
+import type { Mutation, Query } from './api-types';
+import { token } from '$lib/state';
+const RETRY_DELAY = 1000;
+const RETRIES = 5;
+let backoff = 0;
+
+async function mutate(body:any): Promise> {
+ let res: HttpResponse = { status: 0, data: {}, headers: {}, url: '' };
+ for (let tryNum = 0; tryNum < RETRIES; tryNum++) {
+ res = await CapacitorHttp.post({
+ url: 'https://apigira.emel.pt/graphql',
+ headers: {
+ 'User-Agent': 'Gira/3.2.8 (Android 34)',
+ 'content-type': 'application/json',
+ 'authorization': `Bearer ${get(token)?.accessToken}`,
+ },
+ data: body,
+ });
+ if (res.status >= 200 && res.status < 300) {
+ console.debug(body, res);
+ backoff = RETRY_DELAY;
+ return res.data.data as Promise>;
+ } else {
+ console.debug('error in mutate', res);
+ }
+ await new Promise(resolve => setTimeout(resolve, backoff += 1000));
+ }
+ console.error('failed mutation with body', body, res);
+ throw {
+ errors: res.data.errors,
+ status: res.status,
+ } as ThrownError;
+}
+
+async function query(body:any): Promise> {
+ let res: HttpResponse = { status: 0, data: {}, headers: {}, url: '' };
+ for (let tryNum = 0; tryNum < RETRIES; tryNum++) {
+ res = await CapacitorHttp.post({
+ url: 'https://apigira.emel.pt/graphql',
+ headers: {
+ 'User-Agent': 'Gira/3.2.8 (Android 34)',
+ 'content-type': 'application/json',
+ 'authorization': `Bearer ${get(token)?.accessToken}`,
+ },
+ data: body,
+ });
+ if (res.status >= 200 && res.status < 300) {
+ console.debug(body, res);
+ backoff = RETRY_DELAY;
+ return res.data.data as Promise>;
+ } else {
+ console.debug('error in query', res);
+ }
+ await new Promise(resolve => setTimeout(resolve, backoff += 1000));
+ }
+ console.error('failed query with body', body, res);
+ throw {
+ errors: res.data.errors,
+ status: res.status,
+ } as ThrownError;
+}
+
+// async getStations(): Promise> {
+// const req = query<['getStations']>({
+// 'operationName': 'getStations',
+// 'variables': {},
+// 'query': 'query getStations {getStations {code, description, latitude, longitude, name, bikes, docks, serialNumber, assetStatus }}',
+// });
+// return req;
+// }
+
+export function getStationInfo(stationId: string): Promise> {
+ const req = query<['getBikes', 'getDocks']>({
+ 'variables': { input: stationId },
+ 'query': `query {
+ getBikes(input: "${stationId}") { battery, code, name, kms, serialNumber, type, parent }
+ getDocks(input: "${stationId}") { ledStatus, lockStatus, serialNumber, code, name }
+ }`,
+ });
+ return req;
+}
+
+// async getBikes(stationId: string): Promise> {
+// const req = query<['getBikes']>({
+// 'variables': { input: stationId },
+// 'query': `query ($input: String) { getBikes(input: $input) { type, kms, battery, serialNumber, assetType, assetStatus, assetCondition, parent, warehouse, zone, location, latitude, longitude, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version }}`,
+// });
+// return req;
+// }
+
+// async getDocks(stationId: string): Promise> {
+// const req = query<['getDocks']>({
+// 'variables': { input: stationId },
+// 'query': `query ($input: String) { getDocks(input: $input) { ledStatus, lockStatus, serialNumber, assetType, assetStatus, assetCondition, parent, warehouse, zone, location, latitude, longitude, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version }}`,
+// });
+// return req;
+// }
+
+export async function reserveBike(serialNumber: string) {
+ if (dev && (await Preferences.get({ key: 'settings/mockUnlock' })).value === 'true') {
+ console.debug('mock reserveBike');
+ return { reserveBike: true };
+ } else {
+ const req = mutate<['reserveBike']>({
+ 'variables': { input: serialNumber },
+ 'query': `mutation ($input: String) { reserveBike(input: $input) }`,
+ });
+ return req;
+ }
+}
+
+export async function cancelBikeReserve() {
+ const req = mutate<['cancelBikeReserve']>({
+ 'variables': {},
+ 'query': `mutation { cancelBikeReserve }`,
+ });
+ return req;
+}
+
+export async function startTrip() {
+ if (dev && (await Preferences.get({ key: 'settings/mockUnlock' })).value === 'true') {
+ console.debug('mock startTrip');
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ return { startTrip: true };
+ } else {
+ const req = mutate<['startTrip']>({
+ 'variables': {},
+ 'query': `mutation { startTrip }`,
+ });
+ return req;
+ }
+}
+// // returns an int or float of the active trip cost
+// async get_active_trip_cost(){
+// response = await make_post_request("https://apigira.emel.pt/graphql", JSON.stringify({
+// "operationName": "activeTripCost",
+// "variables": {},
+// "query": "query activeTripCost {activeTripCost}"
+// }), user.accessToken)
+// return response.data.activeTripCost
+// }
+
+// async getActiveTripCost() {
+// const req = function query<['activeTripCost']>({
+// 'variables': {},
+// 'query': `query { activeTripCost }`,
+// });
+// return req;
+// }
+
+// async getPointsAndBalance() {
+// const req = function query<['client']>({
+// 'variables': {},
+// 'query': `query { client { balance, bonus } }`,
+// });
+// return req;
+// }
+
+// async updateAccountInfo() {
+// getPointsAndBalance().then(this.ingestAccountInfo);
+// }
+
+// async getSubscriptions() {
+// const req = function query<['activeUserSubscriptions']>({
+// 'variables': {},
+// 'query': `query { activeUserSubscriptions { expirationDate subscriptionStatus name type active } }`,
+// });
+// return req;
+// }
+
+// async updateSubscriptions() {
+// getSubscriptions().then(this.function ingestSubscriptions);
+// }
+
+export async function getTrip(tripCode:string) {
+ const req = query<['getTrip']>({
+ 'variables': { input: tripCode },
+ 'query': `query ($input: String) { getTrip(input: $input) { user, asset, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } }`,
+ });
+ return req;
+}
+
+export async function getActiveTripInfo() {
+ const req = query<['activeTrip']>({
+ 'variables': {},
+ 'query': `query { activeTrip { user, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } }`,
+ });
+ return req;
+}
+
+export async function getTripHistory(pageNum:number, pageSize:number) {
+ const req = query<['tripHistory']>({
+ 'variables': { input: { _pageNum: pageNum, _pageSize: pageSize } },
+ 'query': `query ($input: PageInput) { tripHistory(pageInput: $input) { code, startDate, endDate, rating, bikeName, startLocation, endLocation, bonus, usedPoints, cost, bikeType } }`,
+ });
+ return req;
+}
+
+export async function getUnratedTrips(pageNum:number, pageSize:number) {
+ const req = query<['unratedTrips']>({
+ 'variables': { input: { _pageNum: pageNum, _pageSize: pageSize } },
+ 'query': `query ($input: PageInput) { unratedTrips(pageInput: $input) { code, startDate, endDate, rating, startLocation, endLocation, cost, costBonus, asset } }`,
+ });
+ return req;
+}
+
+export async function rateTrip(tripCode:string, tripRating:number, tripComment?:string, tripAttachment?:File) {
+ if (tripComment === undefined) tripComment = '';
+ const actualAttachment = tripAttachment === undefined ? null : tripAttachment;
+ const req = mutate<['rateTrip']>({
+ 'variables': {
+ in: {
+ code: tripCode,
+ rating: tripRating,
+ description: tripComment,
+ attachment: actualAttachment !== null ? {
+ bytes: actualAttachment?.arrayBuffer() ?? null,
+ fileName: `img_${tripCode}.png`,
+ mimeType: 'image/png',
+ } : null,
+ },
+ },
+ 'query': `mutation ($in: RateTrip_In) { rateTrip(in: $in) }`,
+ });
+ return req;
+}
+
+export async function tripPayWithNoPoints(tripCode: string) {
+ const req = mutate<['tripPayWithNoPoints']>({
+ 'variables': { input: tripCode },
+ 'query': `mutation ($input: String) { tripPayWithNoPoints(input: $input) }`,
+ });
+ return req;
+}
+
+export async function tripPayWithPoints(tripCode:string) {
+ const req = mutate<['tripPayWithPoints']>({
+ 'variables': { input: tripCode },
+ 'query': `mutation ($input: String) { tripPayWithPoints(input: $input) }`,
+ });
+ return req;
+}
+
+export async function fullOnetimeInfo(): Promise> {
+ let megaQuery = `query {`;
+ megaQuery += `getStations {code, description, latitude, longitude, name, bikes, docks, serialNumber, assetStatus } `;
+ megaQuery += `client { balance, bonus } `;
+ megaQuery += `activeUserSubscriptions { expirationDate subscriptionStatus name type active } `;
+ megaQuery += `activeTrip { user, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } `;
+ megaQuery += `unratedTrips(pageInput: { _pageNum: 0, _pageSize: 1 }) { code, startDate, endDate, rating, startLocation, endLocation, cost, costBonus, asset } `;
+ megaQuery += `tripHistory(pageInput: { _pageNum: 0, _pageSize: 1 }) { code, startDate, endDate, rating, bikeName, startLocation, endLocation, bonus, usedPoints, cost, bikeType } `;
+ megaQuery += `}`;
+
+ const req = await query<['getStations', 'client', 'activeUserSubscriptions', 'activeTrip', 'unratedTrips', 'tripHistory']>({
+ 'variables': {},
+ 'query': megaQuery,
+ });
+ return req;
+}
\ No newline at end of file
diff --git a/src/lib/gira-api/index.ts b/src/lib/gira-api/index.ts
deleted file mode 100644
index 7682a4e..0000000
--- a/src/lib/gira-api/index.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-import { dev } from '$app/environment';
-import { accountInfo, currentTrip, stations, token, tripRating, type StationInfo } from '$lib/stores';
-import { CapacitorHttp, type HttpResponse } from '@capacitor/core';
-import { get } from 'svelte/store';
-import type { Mutation, Query } from './types';
-import { Preferences } from '@capacitor/preferences';
-type Q = {[K in T[number]]:Query[K]};
-type M = {[K in T[number]]:Mutation[K]};
-export type ThrownError = {
- errors: {message:string}[];
- status: number;
-};
-
-const retries = 5;
-const retryDelay = 1000;
-let backoff = 1000;
-
-async function mutate(body:any): Promise> {
- let res: HttpResponse = { status: 0, data: {}, headers: {}, url: '' };
- for (let tryNum = 0; tryNum < retries; tryNum++) {
- res = await CapacitorHttp.post({
- url: 'https://apigira.emel.pt/graphql',
- headers: {
- 'User-Agent': 'Gira/3.2.8 (Android 34)',
- 'content-type': 'application/json',
- 'authorization': `Bearer ${get(token)?.accessToken}`,
- },
- data: body,
- });
- if (res.status >= 200 && res.status < 300) {
- console.debug(body, res);
- backoff = retryDelay;
- return res.data.data as Promise>;
- } else {
- console.debug('error in mutate', res);
- }
- await new Promise(resolve => setTimeout(resolve, backoff += 1000));
- }
- console.error('failed mutation with body', body, res);
- throw {
- errors: res.data.errors,
- status: res.status,
- } as ThrownError;
-}
-async function query(body:any): Promise> {
- let res: HttpResponse = { status: 0, data: {}, headers: {}, url: '' };
- for (let tryNum = 0; tryNum < retries; tryNum++) {
- res = await CapacitorHttp.post({
- url: 'https://apigira.emel.pt/graphql',
- headers: {
- 'User-Agent': 'Gira/3.2.8 (Android 34)',
- 'content-type': 'application/json',
- 'authorization': `Bearer ${get(token)?.accessToken}`,
- },
- data: body,
- });
- if (res.status >= 200 && res.status < 300) {
- console.debug(body, res);
- backoff = retryDelay;
- return res.data.data as Promise>;
- } else {
- console.debug('error in query', res);
- }
- await new Promise(resolve => setTimeout(resolve, backoff += 1000));
- }
- console.error('failed query with body', body, res);
- throw {
- errors: res.data.errors,
- status: res.status,
- } as ThrownError;
-}
-
-export async function getStations(): Promise> {
- const req = query<['getStations']>({
- 'operationName': 'getStations',
- 'variables': {},
- 'query': 'query getStations {getStations {code, description, latitude, longitude, name, bikes, docks, serialNumber, assetStatus }}',
- });
- return req;
-}
-
-export async function updateStations() {
- getStations().then(ingestStations);
-}
-
-function ingestStations(maybeStations:Q<['getStations']>) {
- if (maybeStations.getStations === null || maybeStations.getStations === undefined) return;
- const stationsList:StationInfo[] = [];
- maybeStations.getStations.forEach(station => {
- if (station === null || station === undefined) return;
- const { code, name, description, latitude, longitude, bikes, docks, serialNumber, assetStatus } = station;
- if (code === null || code === undefined || name === null ||
- name === undefined || description === undefined ||
- latitude === null || latitude === undefined || longitude === null ||
- longitude === undefined || bikes === null || bikes === undefined ||
- docks === null || docks === undefined || serialNumber === null ||
- serialNumber === undefined || assetStatus === null || assetStatus === undefined
- ) {
- console.error('invalid station', station);
- return;
- }
- stationsList.push({ code, name, description, latitude, longitude, bikes, docks, serialNumber, assetStatus });
- });
- stations.set(stationsList);
-}
-
-export async function getStationInfo(stationId: string): Promise> {
- const req = query<['getBikes', 'getDocks']>({
- 'variables': { input: stationId },
- 'query': `query {
- getBikes(input: "${stationId}") { battery, code, name, kms, serialNumber, type, parent }
- getDocks(input: "${stationId}") { ledStatus, lockStatus, serialNumber, code, name }
- }`,
- });
- return req;
-}
-
-export async function getBikes(stationId: string): Promise> {
- const req = query<['getBikes']>({
- 'variables': { input: stationId },
- 'query': `query ($input: String) { getBikes(input: $input) { type, kms, battery, serialNumber, assetType, assetStatus, assetCondition, parent, warehouse, zone, location, latitude, longitude, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version }}`,
- });
- return req;
-}
-
-export async function getDocks(stationId: string): Promise> {
- const req = query<['getDocks']>({
- 'variables': { input: stationId },
- 'query': `query ($input: String) { getDocks(input: $input) { ledStatus, lockStatus, serialNumber, assetType, assetStatus, assetCondition, parent, warehouse, zone, location, latitude, longitude, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version }}`,
- });
- return req;
-}
-
-export async function reserveBike(serialNumber: string) {
- if (dev && (await Preferences.get({ key: 'settings/mockUnlock' })).value === 'true') {
- console.debug('mock reserveBike');
- return { reserveBike: true };
- } else {
- const req = mutate<['reserveBike']>({
- 'variables': { input: serialNumber },
- 'query': `mutation ($input: String) { reserveBike(input: $input) }`,
- });
- return req;
- }
-}
-
-export async function cancelBikeReserve() {
- const req = mutate<['cancelBikeReserve']>({
- 'variables': {},
- 'query': `mutation { cancelBikeReserve }`,
- });
- return req;
-}
-
-export async function startTrip() {
- if (dev && (await Preferences.get({ key: 'settings/mockUnlock' })).value === 'true') {
- console.debug('mock startTrip');
- await new Promise(resolve => setTimeout(resolve, 2000));
- return { startTrip: true };
- } else {
- const req = mutate<['startTrip']>({
- 'variables': {},
- 'query': `mutation { startTrip }`,
- });
- return req;
- }
-}
-// // returns an int or float of the active trip cost
-// async function get_active_trip_cost(){
-// response = await make_post_request("https://apigira.emel.pt/graphql", JSON.stringify({
-// "operationName": "activeTripCost",
-// "variables": {},
-// "query": "query activeTripCost {activeTripCost}"
-// }), user.accessToken)
-// return response.data.activeTripCost
-// }
-
-export async function getActiveTripCost() {
- const req = query<['activeTripCost']>({
- 'variables': {},
- 'query': `query { activeTripCost }`,
- });
- return req;
-}
-
-export async function getPointsAndBalance() {
- const req = query<['client']>({
- 'variables': {},
- 'query': `query { client { balance, bonus } }`,
- });
- return req;
-}
-
-export async function updateAccountInfo() {
- getPointsAndBalance().then(ingestAccountInfo);
-}
-
-export function ingestAccountInfo(maybePointsAndBalance:Q<['client']>) {
- if (maybePointsAndBalance.client === null || maybePointsAndBalance.client === undefined || maybePointsAndBalance.client.length <= 0) return;
- const { balance, bonus } = maybePointsAndBalance.client[0]!;
- if (balance === null || balance === undefined || bonus === null || bonus === undefined) return;
- accountInfo.update(ai => (
- { subscription: ai?.subscription ?? null, balance, bonus }
- ));
-}
-
-export async function getSubscriptions() {
- const req = query<['activeUserSubscriptions']>({
- 'variables': {},
- 'query': `query { activeUserSubscriptions { expirationDate subscriptionStatus name type active } }`,
- });
- return req;
-}
-
-export async function updateSubscriptions() {
- getSubscriptions().then(ingestSubscriptions);
-}
-
-export async function ingestSubscriptions(maybeSubscriptions:Q<['activeUserSubscriptions']>) {
- if (maybeSubscriptions.activeUserSubscriptions === null || maybeSubscriptions.activeUserSubscriptions === undefined || maybeSubscriptions.activeUserSubscriptions.length <= 0) return;
- const { active, expirationDate, name, subscriptionStatus, type } = maybeSubscriptions.activeUserSubscriptions[0]!;
- accountInfo.update(ai => (
- { balance: ai?.balance ?? 0, bonus: ai?.bonus ?? 0, subscription: { active: active!, expirationDate: new Date(expirationDate), name: name!, subscriptionStatus: subscriptionStatus!, type: type ?? 'unknown' } }
- ));
-}
-
-export async function getTrip(tripCode:string) {
- const req = query<['getTrip']>({
- 'variables': { input: tripCode },
- 'query': `query ($input: String) { getTrip(input: $input) { user, asset, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } }`,
- });
- return req;
-}
-
-export async function getActiveTripInfo() {
- const req = query<['activeTrip']>({
- 'variables': {},
- 'query': `query { activeTrip { user, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } }`,
- });
- return req;
-}
-
-export async function updateActiveTripInfo() {
- getActiveTripInfo().then(ingestActiveTripInfo);
-}
-function ingestActiveTripInfo(maybeTrips:Q<['activeTrip']>) {
- if (maybeTrips.activeTrip === null || maybeTrips.activeTrip === undefined || maybeTrips.activeTrip.code === 'no_trip' || maybeTrips.activeTrip.asset === 'dummy') {
- if (get(currentTrip)?.confirmed || Date.now() - (get(currentTrip)?.startDate?.getTime() ?? 0) > 30000) {
- currentTrip.set(null);
- }
- return;
- }
- const {
- // asset,
- startDate,
- code,
- // user,
- // endDate,
- // startLocation,
- // endLocation,
- // rating,
- // photo,
- // cost,
- // startOccupation,
- // endOccupation,
- // totalBonus,
- // client,
- // costBonus,
- // comment,
- // compensationTime,
- // endTripDock,
- // tripStatus,
- // name,
- // description,
- // creationDate,
- // createdBy,
- // updateDate,
- // updatedBy,
- // defaultOrder,
- // version,
- } = maybeTrips.activeTrip!;
- currentTrip.update(ct => ct ? {
- code: code!,
- bikePlate: ct.bikePlate,
- startPos: ct.startPos,
- destination: ct.destination,
- traveledDistanceKm: ct.traveledDistanceKm,
- distanceLeft: ct.distanceLeft,
- speed: ct.speed,
- startDate: new Date(startDate!),
- predictedEndDate: ct.predictedEndDate,
- arrivalTime: ct.predictedEndDate,
- finished: false,
- confirmed: true,
- pathTaken: ct.pathTaken,
- } : {
- code: code!,
- bikePlate: null,
- startPos: null,
- destination: null,
- traveledDistanceKm: 0,
- distanceLeft: null,
- speed: 0,
- startDate: new Date(startDate!),
- predictedEndDate: null,
- arrivalTime: null,
- finished: false,
- confirmed: true,
- pathTaken: [],
- });
-}
-
-export async function getTripHistory(pageNum:number, pageSize:number) {
- const req = query<['tripHistory']>({
- 'variables': { input: { _pageNum: pageNum, _pageSize: pageSize } },
- 'query': `query ($input: PageInput) { tripHistory(pageInput: $input) { code, startDate, endDate, rating, bikeName, startLocation, endLocation, bonus, usedPoints, cost, bikeType } }`,
- });
- return req;
-}
-
-export async function getUnratedTrips(pageNum:number, pageSize:number) {
- const req = query<['unratedTrips']>({
- 'variables': { input: { _pageNum: pageNum, _pageSize: pageSize } },
- 'query': `query ($input: PageInput) { unratedTrips(pageInput: $input) { code, startDate, endDate, rating, startLocation, endLocation, cost, costBonus, asset } }`,
- });
- return req;
-}
-
-export async function updateLastUnratedTrip() {
- const q = `query ($input: PageInput) { unratedTrips(pageInput: $input) { code, startDate, endDate, rating, startLocation, endLocation, cost, costBonus, asset }
- tripHistory(pageInput: $input) { code, startDate, endDate, rating, bikeName, startLocation, endLocation, bonus, usedPoints, cost, bikeType } }`;
-
- const req = await query<['unratedTrips', 'tripHistory']>({
- 'variables': { input: { _pageNum: 0, _pageSize: 1 } },
- 'query': q,
- });
- ingestLastUnratedTrip(req);
-}
-
-function ingestLastUnratedTrip(lastTripData:Q<['unratedTrips', 'tripHistory']>) {
- if (lastTripData.unratedTrips === null || lastTripData.unratedTrips === undefined || lastTripData.unratedTrips.length <= 0) return;
- const unratedTrip = lastTripData.unratedTrips[0];
- if (unratedTrip == null || unratedTrip.code == null || unratedTrip.asset == null) return;
- const endToNow = (new Date).getTime() - new Date(unratedTrip.endDate).getTime();
- // check if 24h have passed
- if (!(endToNow < 24 * 60 * 60 * 1000)) return;
- let bikePlate;
- if (lastTripData.tripHistory !== null && lastTripData.tripHistory !== undefined && lastTripData.tripHistory.length > 0) {
- const lastTrip = lastTripData.tripHistory[0];
- if (lastTrip) {
- const lastTripCode = lastTrip?.code;
- bikePlate = lastTrip.bikeName;
- if (lastTripCode !== unratedTrip.code) return;
- }
- }
-
- tripRating.set({
- currentRating: {
- code: unratedTrip.code,
- // probably have to translate asset to bike id
- bikePlate: bikePlate ?? '???',
- startDate: new Date(unratedTrip.startDate),
- endDate: new Date(unratedTrip.endDate),
- tripPoints: unratedTrip.costBonus || 0,
- },
- });
-}
-// input RateTrip_In {
-// code: String
-// rating: Int
-// description: String
-// attachment: Attachment
-// }
-export async function rateTrip(tripCode:string, tripRating:number, tripComment?:string, tripAttachment?:File) {
- if (tripComment === undefined) tripComment = '';
- const actualAttachment = tripAttachment === undefined ? null : tripAttachment;
- const req = mutate<['rateTrip']>({
- 'variables': {
- in: {
- code: tripCode,
- rating: tripRating,
- description: tripComment,
- attachment: actualAttachment !== null ? {
- bytes: actualAttachment?.arrayBuffer() ?? null,
- fileName: `img_${tripCode}.png`,
- mimeType: 'image/png',
- } : null,
- },
- },
- 'query': `mutation ($in: RateTrip_In) { rateTrip(in: $in) }`,
- });
- return req;
-}
-
-export async function tripPayWithNoPoints(tripCode: string) {
- const req = mutate<['tripPayWithNoPoints']>({
- 'variables': { input: tripCode },
- 'query': `mutation ($input: String) { tripPayWithNoPoints(input: $input) }`,
- });
- return req;
-}
-export async function tripPayWithPoints(tripCode:string) {
- const req = mutate<['tripPayWithPoints']>({
- 'variables': { input: tripCode },
- 'query': `mutation ($input: String) { tripPayWithPoints(input: $input) }`,
- });
- return req;
-}
-
-export async function updateOnetimeInfo() {
- let megaQuery = `query {`;
- megaQuery += `getStations {code, description, latitude, longitude, name, bikes, docks, serialNumber, assetStatus } `;
- megaQuery += `client { balance, bonus } `;
- megaQuery += `activeUserSubscriptions { expirationDate subscriptionStatus name type active } `;
- megaQuery += `activeTrip { user, startDate, endDate, startLocation, endLocation, distance, rating, photo, cost, startOccupation, endOccupation, totalBonus, client, costBonus, comment, compensationTime, endTripDock, tripStatus, code, name, description, creationDate, createdBy, updateDate, updatedBy, defaultOrder, version } `;
- megaQuery += `unratedTrips(pageInput: { _pageNum: 0, _pageSize: 1 }) { code, startDate, endDate, rating, startLocation, endLocation, cost, costBonus, asset } `;
- megaQuery += `tripHistory(pageInput: { _pageNum: 0, _pageSize: 1 }) { code, startDate, endDate, rating, bikeName, startLocation, endLocation, bonus, usedPoints, cost, bikeType } `;
- megaQuery += `}`;
-
- const req = await query<['getStations', 'client', 'activeUserSubscriptions', 'activeTrip', 'unratedTrips', 'tripHistory']>({
- 'variables': {},
- 'query': megaQuery,
- });
-
- ingestAccountInfo(req);
- ingestSubscriptions(req);
- ingestActiveTripInfo(req);
- ingestStations(req);
- ingestLastUnratedTrip(req);
-}
\ No newline at end of file
diff --git a/src/lib/gira-api/ws-types.ts b/src/lib/gira-api/ws-types.ts
index 6340d8c..037e17a 100644
--- a/src/lib/gira-api/ws-types.ts
+++ b/src/lib/gira-api/ws-types.ts
@@ -20,7 +20,7 @@ export type ActiveTripSubscription = {
bike: string;
startDate: Date;
endDate: Date|null;
- cost: null;
+ cost: null|number;
finished: boolean;
canPayWithMoney: boolean|null;
canUsePoints: boolean|null;
diff --git a/src/lib/gira-api/ws.ts b/src/lib/gira-api/ws.ts
index 90a42ee..c226672 100644
--- a/src/lib/gira-api/ws.ts
+++ b/src/lib/gira-api/ws.ts
@@ -1,27 +1,21 @@
-import { currentTrip, stations, token, tripRating } from '$lib/stores';
+import { token } from '$lib/state';
import { get } from 'svelte/store';
-import type { ActiveTripSubscription, WSEvent } from './ws-types';
-import { tripPayWithNoPoints, tripPayWithPoints } from '.';
-import { currentPos } from '$lib/location';
-export let ws: WebSocket;
-
-function randomUUID() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
- const r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8;
- return v.toString(16);
- });
-}
+import type { WSEvent } from './ws-types';
+import { randomUUID } from '$lib/utils';
+import { updateWithTripMessage } from '$lib/state/helper';
+import { ingestStations } from '$lib/state/mutate';
+let ws:WebSocket;
export function startWS() {
console.debug('starting ws');
const tokens = get(token);
const access = tokens?.accessToken;
if (!access) return;
-
if (ws) {
- ws.onclose = () => {};
- if (ws.readyState === WebSocket.OPEN) ws.close();
+ if (ws.readyState === WebSocket.CONNECTING) return;
+ if (ws.readyState === WebSocket.OPEN) return;
}
+
ws = new WebSocket('wss://apigira.emel.pt/graphql', 'graphql-ws');
ws.onopen = () => {
backoff = 0;
@@ -60,10 +54,10 @@ export function startWS() {
const data = payload.data;
if (data.operationalStationsSubscription) {
console.debug('updated stations with websocket');
- stations.set(data.operationalStationsSubscription);
+ ingestStations({ getStations: data.operationalStationsSubscription });
} else if (data.activeTripSubscription) {
const recvTrip = data.activeTripSubscription;
- ingestTripMessage(recvTrip);
+ updateWithTripMessage(recvTrip);
} else if (data.serverDate) {
console.debug('serverdate', data.serverDate.date);
console.debug('serverdate diff', new Date(data.serverDate.date).getTime() - Date.now(), 'ms');
@@ -73,12 +67,12 @@ export function startWS() {
};
let backoff = 0;
- function restartWS() {
+ const restartWS = () => {
setTimeout(() => {
startWS();
backoff += 1000;
}, backoff);
- }
+ };
ws.onclose = e => {
console.debug('ws closed', e);
restartWS();
@@ -87,74 +81,4 @@ export function startWS() {
console.debug('ws error', e);
restartWS();
};
-}
-function ingestTripMessage(recvTrip:ActiveTripSubscription) {
- console.debug('ingesting trip message from ws', recvTrip);
- if (recvTrip.code === 'no_trip' || recvTrip.bike === 'dummy') {
- if (get(currentTrip)?.confirmed || Date.now() - (get(currentTrip)?.startDate?.getTime() ?? 0) > 30000) {
- currentTrip.set(null);
- }
- return;
- }
-
- if (recvTrip.finished) {
- if (recvTrip.canUsePoints) tripPayWithPoints(recvTrip.code);
- else if (recvTrip.canPayWithMoney) tripPayWithNoPoints(recvTrip.code);
- }
-
- const ctrip = get(currentTrip);
- if (recvTrip.code === ctrip?.code) ingestCurrentTripUpdate(recvTrip);
- else ingestOtherTripUpdate(recvTrip);
-}
-
-function ingestCurrentTripUpdate(recvTrip:ActiveTripSubscription) {
- currentTrip.update(trip => {
- // if trip finished, rate, else, update trip stuff
- if (recvTrip.finished) {
- currentTrip.set(null);
- tripRating.update(rating => {
- rating.currentRating = {
- code: recvTrip.code,
- bikePlate: recvTrip.bike,
- startDate: new Date(recvTrip.startDate),
- endDate: new Date(recvTrip.endDate ?? 0),
- tripPoints: recvTrip.tripPoints ?? 0,
- };
- return rating;
- });
- return null;
- } else {
- if (trip === null) throw new Error('trip is null in impossible place');
- return {
- ...trip,
- startDate: new Date(recvTrip.startDate),
- bikePlate: recvTrip.bike,
- code: recvTrip.code,
- confirmed: true,
- };
- }
- });
-}
-function ingestOtherTripUpdate(recvTrip:ActiveTripSubscription) {
- if (recvTrip.finished) return;
- const p = get(currentPos);
- currentTrip.set({
- startDate: new Date(recvTrip.startDate),
- bikePlate: recvTrip.bike,
- code: recvTrip.code,
- finished: recvTrip.finished,
- startPos: null,
- destination: null,
- traveledDistanceKm: 0,
- distanceLeft: null,
- speed: 0,
- predictedEndDate: null,
- arrivalTime: null,
- confirmed: true,
- pathTaken: p ? [{
- lat: p.coords.latitude,
- lng: p.coords.longitude,
- time: new Date,
- }] : [],
- });
}
\ No newline at end of file
diff --git a/src/lib/state/helper.test.ts b/src/lib/state/helper.test.ts
new file mode 100644
index 0000000..180564c
--- /dev/null
+++ b/src/lib/state/helper.test.ts
@@ -0,0 +1,88 @@
+import type { ActiveTripSubscription } from '$lib/gira-api/ws-types';
+import { get } from 'svelte/store';
+import { updateWithTripMessage } from './helper';
+import { beforeAll, describe, expect, it, vi } from 'vitest';
+import { currentTrip } from '.';
+import { tripPayWithNoPoints, tripPayWithPoints } from '$lib/gira-api/__mocks__/api';
+import * as api from '$lib/gira-api/__mocks__/api';
+
+beforeAll(() => {
+ vi.mock('$lib/gira-api/api');
+});
+
+describe('updateWithTripMessage', () => {
+ it('should pay for the trip', () => {
+ const currentDate = new Date;
+ const activeTrip:ActiveTripSubscription = {
+ 'code': 'ABC123',
+ 'bike': 'E0041',
+ 'startDate': new Date(currentDate.getTime() - 2000),
+ 'endDate': null,
+ 'cost': 0,
+ 'finished': false,
+ 'canPayWithMoney': false,
+ 'canUsePoints': false,
+ 'clientPoints': 49170,
+ 'tripPoints': null,
+ 'canceled': false,
+ 'period': 'other',
+ 'periodTime': '116',
+ 'error': 0,
+ };
+ global.Date.now = vi.fn(() => activeTrip.startDate.getTime() + 35000);
+
+ const randomOldTrip:ActiveTripSubscription = {
+ 'code': '8PBL9FVCPH',
+ 'bike': 'E0040',
+ 'startDate': new Date(activeTrip.startDate.getTime() - 36000),
+ 'endDate': new Date(activeTrip.startDate.getTime() - 30000),
+ 'cost': 0,
+ 'finished': true,
+ 'canPayWithMoney': true,
+ 'canUsePoints': false,
+ 'clientPoints': 49170,
+ 'tripPoints': 110,
+ 'canceled': false,
+ 'period': 'other',
+ 'periodTime': '116',
+ 'error': 0,
+ };
+
+ const currentTripEnd:ActiveTripSubscription = {
+ 'code': 'ABC123',
+ 'bike': 'E0041',
+ 'startDate': new Date(currentDate.getTime() - 2000),
+ 'endDate': new Date,
+ 'cost': 0,
+ 'finished': true,
+ 'canPayWithMoney': true,
+ 'canUsePoints': false,
+ 'clientPoints': 49170,
+ 'tripPoints': 110,
+ 'canceled': false,
+ 'period': 'other',
+ 'periodTime': '116',
+ 'error': 0,
+ };
+
+ // set mock
+
+ console.log(api);
+ vi.spyOn(api, 'tripPayWithNoPoints');
+ vi.spyOn(api, 'tripPayWithPoints');
+ updateWithTripMessage(activeTrip);
+ expect(tripPayWithPoints).toHaveBeenCalledTimes(0);
+ expect(tripPayWithNoPoints).toHaveBeenCalledTimes(0);
+ expect(get(currentTrip)?.code).toBe(activeTrip.code);
+
+ updateWithTripMessage(randomOldTrip);
+ expect(tripPayWithPoints).toHaveBeenCalledTimes(0);
+ expect(tripPayWithNoPoints).toHaveBeenCalledTimes(1);
+ expect(get(currentTrip)?.code).toBe(activeTrip.code);
+
+ updateWithTripMessage(currentTripEnd);
+ expect(tripPayWithPoints).toHaveBeenCalledTimes(0);
+ expect(tripPayWithNoPoints).toHaveBeenCalledTimes(2);
+ expect(get(currentTrip)).toBe(null);
+ });
+});
\ No newline at end of file
diff --git a/src/lib/state/helper.ts b/src/lib/state/helper.ts
new file mode 100644
index 0000000..f9fee78
--- /dev/null
+++ b/src/lib/state/helper.ts
@@ -0,0 +1,47 @@
+import { get } from 'svelte/store';
+import { token, userCredentials, accountInfo, currentTrip, user, selectedStation, tripRating } from '.';
+import { ingestAccountInfo, ingestActiveTripInfo, ingestCurrentTripUpdate, ingestLastUnratedTrip, ingestOtherTripUpdate, ingestStations, ingestSubscriptions } from './mutate';
+import type { ActiveTripSubscription } from '$lib/gira-api/ws-types';
+import { fullOnetimeInfo, getActiveTripInfo, tripPayWithNoPoints, tripPayWithPoints } from '$lib/gira-api/api';
+
+export async function updateOnetimeInfo() {
+ const resp = await fullOnetimeInfo();
+ ingestStations(resp);
+ ingestAccountInfo(resp);
+ ingestSubscriptions(resp);
+ ingestActiveTripInfo(resp);
+ ingestLastUnratedTrip(resp);
+}
+
+export function updateActiveTripInfo() {
+ getActiveTripInfo().then(ingestActiveTripInfo);
+}
+export async function logOut() {
+ token.set(null);
+ userCredentials.set(null);
+ accountInfo.set(null);
+ currentTrip.set(null);
+ user.set(null);
+ selectedStation.set(null);
+ tripRating.set({ currentRating: null });
+ // purposefully not settings settings distancelock, since thats annoying when you swap accounts
+}
+
+export function updateWithTripMessage(recvTrip:ActiveTripSubscription) {
+ console.debug('ingesting trip message from ws', recvTrip);
+ if (recvTrip.code === 'no_trip' || recvTrip.bike === 'dummy') {
+ if (get(currentTrip)?.confirmed || Date.now() - (get(currentTrip)?.startDate?.getTime() ?? 0) > 30000) {
+ currentTrip.set(null);
+ }
+ return;
+ }
+
+ if (recvTrip.finished) {
+ if (recvTrip.canUsePoints) tripPayWithPoints(recvTrip.code);
+ else if (recvTrip.canPayWithMoney) tripPayWithNoPoints(recvTrip.code);
+ }
+
+ const ctrip = get(currentTrip);
+ if (recvTrip.code === ctrip?.code) ingestCurrentTripUpdate(recvTrip);
+ else ingestOtherTripUpdate(recvTrip);
+}
\ No newline at end of file
diff --git a/src/lib/stores.ts b/src/lib/state/index.ts
similarity index 89%
rename from src/lib/stores.ts
rename to src/lib/state/index.ts
index dade1b8..868aff8 100644
--- a/src/lib/stores.ts
+++ b/src/lib/state/index.ts
@@ -1,10 +1,10 @@
import { Preferences } from '@capacitor/preferences';
import { get, writable, type Writable } from 'svelte/store';
-import { login, refreshToken, updateUserInfo } from './auth';
-import { updateOnetimeInfo } from './gira-api';
-import { startWS, ws } from './gira-api/ws';
-import { currentPos } from './location';
-import { distanceBetweenCoords } from './utils';
+import { login, refreshToken, updateUserInfo } from '../auth';
+import { currentPos } from '../location';
+import { distanceBetweenCoords } from '../utils';
+import { updateOnetimeInfo } from './helper';
+import { startWS } from '$lib/gira-api/ws';
export type User = {
email: string;
@@ -122,7 +122,7 @@ token.subscribe(async v => {
if (!v) return;
const jwt:JWT = JSON.parse(window.atob(v.accessToken.split('.')[1]));
- if (!ws || ws.readyState === ws.CLOSED) startWS();
+ startWS();
if (get(user) === null) {
updateOnetimeInfo();
updateUserInfo();
@@ -179,15 +179,4 @@ currentPos.subscribe(async v => {
}
return trip;
});
-});
-
-export async function logOut() {
- token.set(null);
- userCredentials.set(null);
- accountInfo.set(null);
- currentTrip.set(null);
- user.set(null);
- selectedStation.set(null);
- tripRating.set({ currentRating: null });
- // purposefully not settings settings distancelock, since thats annoying when you swap accounts
-}
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/lib/state/mutate.ts b/src/lib/state/mutate.ts
new file mode 100644
index 0000000..8e56263
--- /dev/null
+++ b/src/lib/state/mutate.ts
@@ -0,0 +1,191 @@
+import type { Q } from '$lib/gira-api';
+import { get } from 'svelte/store';
+import { stations, type StationInfo, tripRating, currentTrip, accountInfo } from '.';
+import type { ActiveTripSubscription } from '$lib/gira-api/ws-types';
+import { currentPos } from '$lib/location';
+
+export function ingestStations(maybeStations:Q<['getStations']>) {
+ if (maybeStations.getStations === null || maybeStations.getStations === undefined) return;
+ const stationsList:StationInfo[] = [];
+ maybeStations.getStations.forEach(station => {
+ if (station === null || station === undefined) return;
+ const { code, name, description, latitude, longitude, bikes, docks, serialNumber, assetStatus } = station;
+ if (code === null || code === undefined || name === null ||
+ name === undefined || description === undefined ||
+ latitude === null || latitude === undefined || longitude === null ||
+ longitude === undefined || bikes === null || bikes === undefined ||
+ docks === null || docks === undefined || serialNumber === null ||
+ serialNumber === undefined || assetStatus === null || assetStatus === undefined
+ ) {
+ console.error('invalid station', station);
+ return;
+ }
+ stationsList.push({ code, name, description, latitude, longitude, bikes, docks, serialNumber, assetStatus });
+ });
+ stations.set(stationsList);
+}
+
+export function ingestAccountInfo(maybePointsAndBalance:Q<['client']>) {
+ if (maybePointsAndBalance.client === null || maybePointsAndBalance.client === undefined || maybePointsAndBalance.client.length <= 0) return;
+ const { balance, bonus } = maybePointsAndBalance.client[0]!;
+ if (balance === null || balance === undefined || bonus === null || bonus === undefined) return;
+ accountInfo.update(ai => (
+ { subscription: ai?.subscription ?? null, balance, bonus }
+ ));
+}
+
+export async function ingestSubscriptions(maybeSubscriptions:Q<['activeUserSubscriptions']>) {
+ if (maybeSubscriptions.activeUserSubscriptions === null || maybeSubscriptions.activeUserSubscriptions === undefined || maybeSubscriptions.activeUserSubscriptions.length <= 0) return;
+ const { active, expirationDate, name, subscriptionStatus, type } = maybeSubscriptions.activeUserSubscriptions[0]!;
+ accountInfo.update(ai => (
+ { balance: ai?.balance ?? 0, bonus: ai?.bonus ?? 0, subscription: { active: active!, expirationDate: new Date(expirationDate), name: name!, subscriptionStatus: subscriptionStatus!, type: type ?? 'unknown' } }
+ ));
+}
+
+export function ingestActiveTripInfo(maybeTrips:Q<['activeTrip']>) {
+ if (maybeTrips.activeTrip === null || maybeTrips.activeTrip === undefined || maybeTrips.activeTrip.code === 'no_trip' || maybeTrips.activeTrip.asset === 'dummy') {
+ if (get(currentTrip)?.confirmed || Date.now() - (get(currentTrip)?.startDate?.getTime() ?? 0) > 30000) {
+ currentTrip.set(null);
+ }
+ return;
+ }
+ const {
+ // asset,
+ startDate,
+ code,
+ // user,
+ // endDate,
+ // startLocation,
+ // endLocation,
+ // rating,
+ // photo,
+ // cost,
+ // startOccupation,
+ // endOccupation,
+ // totalBonus,
+ // client,
+ // costBonus,
+ // comment,
+ // compensationTime,
+ // endTripDock,
+ // tripStatus,
+ // name,
+ // description,
+ // creationDate,
+ // createdBy,
+ // updateDate,
+ // updatedBy,
+ // defaultOrder,
+ // version,
+ } = maybeTrips.activeTrip!;
+ currentTrip.update(ct => ct ? {
+ code: code!,
+ bikePlate: ct.bikePlate,
+ startPos: ct.startPos,
+ destination: ct.destination,
+ traveledDistanceKm: ct.traveledDistanceKm,
+ distanceLeft: ct.distanceLeft,
+ speed: ct.speed,
+ startDate: new Date(startDate!),
+ predictedEndDate: ct.predictedEndDate,
+ arrivalTime: ct.predictedEndDate,
+ finished: false,
+ confirmed: true,
+ pathTaken: ct.pathTaken,
+ } : {
+ code: code!,
+ bikePlate: null,
+ startPos: null,
+ destination: null,
+ traveledDistanceKm: 0,
+ distanceLeft: null,
+ speed: 0,
+ startDate: new Date(startDate!),
+ predictedEndDate: null,
+ arrivalTime: null,
+ finished: false,
+ confirmed: true,
+ pathTaken: [],
+ });
+}
+export function ingestLastUnratedTrip(lastTripData:Q<['unratedTrips', 'tripHistory']>) {
+ if (lastTripData.unratedTrips === null || lastTripData.unratedTrips === undefined || lastTripData.unratedTrips.length <= 0) return;
+ const unratedTrip = lastTripData.unratedTrips[0];
+ if (unratedTrip == null || unratedTrip.code == null || unratedTrip.asset == null) return;
+ const endToNow = (new Date).getTime() - new Date(unratedTrip.endDate).getTime();
+ // check if 24h have passed
+ if (!(endToNow < 24 * 60 * 60 * 1000)) return;
+ let bikePlate;
+ if (lastTripData.tripHistory !== null && lastTripData.tripHistory !== undefined && lastTripData.tripHistory.length > 0) {
+ const lastTrip = lastTripData.tripHistory[0];
+ if (lastTrip) {
+ const lastTripCode = lastTrip?.code;
+ bikePlate = lastTrip.bikeName;
+ if (lastTripCode !== unratedTrip.code) return;
+ }
+ }
+
+ tripRating.set({
+ currentRating: {
+ code: unratedTrip.code,
+ // probably have to translate asset to bike id
+ bikePlate: bikePlate ?? '???',
+ startDate: new Date(unratedTrip.startDate),
+ endDate: new Date(unratedTrip.endDate),
+ tripPoints: unratedTrip.costBonus || 0,
+ },
+ });
+}
+
+export function ingestCurrentTripUpdate(recvTrip:ActiveTripSubscription) {
+ currentTrip.update(trip => {
+ // if trip finished, rate, else, update trip stuff
+ if (recvTrip.finished) {
+ currentTrip.set(null);
+ tripRating.update(rating => {
+ rating.currentRating = {
+ code: recvTrip.code,
+ bikePlate: recvTrip.bike,
+ startDate: new Date(recvTrip.startDate),
+ endDate: new Date(recvTrip.endDate ?? 0),
+ tripPoints: recvTrip.tripPoints ?? 0,
+ };
+ return rating;
+ });
+ return null;
+ } else {
+ if (trip === null) throw new Error('trip is null in impossible place');
+ return {
+ ...trip,
+ startDate: new Date(recvTrip.startDate),
+ bikePlate: recvTrip.bike,
+ code: recvTrip.code,
+ confirmed: true,
+ };
+ }
+ });
+}
+
+export function ingestOtherTripUpdate(recvTrip:ActiveTripSubscription) {
+ if (recvTrip.finished) return;
+ const p = get(currentPos);
+ currentTrip.set({
+ startDate: new Date(recvTrip.startDate),
+ bikePlate: recvTrip.bike,
+ code: recvTrip.code,
+ finished: recvTrip.finished,
+ startPos: null,
+ destination: null,
+ traveledDistanceKm: 0,
+ distanceLeft: null,
+ speed: 0,
+ predictedEndDate: null,
+ arrivalTime: null,
+ confirmed: true,
+ pathTaken: p ? [{
+ lat: p.coords.latitude,
+ lng: p.coords.longitude,
+ time: new Date,
+ }] : [],
+ });
+}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 8fe0c10..6a7d364 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -15,4 +15,11 @@ export function distanceBetweenCoords(lat1:number, lon1:number, lat2:number, lon
export function formatDistance(distance:number) {
if (distance < 1) return `${(distance * 1000).toFixed(0)}m`;
return `${distance.toLocaleString(undefined, { maximumFractionDigits: 2, useGrouping: false })}km`;
+}
+
+export function randomUUID() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+ const r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8;
+ return v.toString(16);
+ });
}
\ No newline at end of file
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 3fd0f78..a1774f5 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,5 +1,5 @@