From a7032692f13f8f44a8188222141531d5ffd39fe4 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Thu, 18 Jun 2026 19:17:12 +1000 Subject: [PATCH] feat(driver-pwa): auth store + ky client + typed driver API (zod) Co-Authored-By: claude-flow --- driver-pwa/frontend/src/api/client.ts | 24 ++++++++++++++ driver-pwa/frontend/src/api/driver.test.ts | 23 ++++++++++++++ driver-pwa/frontend/src/api/driver.ts | 37 ++++++++++++++++++++++ driver-pwa/frontend/src/store/auth.ts | 21 ++++++++++++ driver-pwa/frontend/src/vite-env.d.ts | 9 ++++++ 5 files changed, 114 insertions(+) create mode 100644 driver-pwa/frontend/src/api/client.ts create mode 100644 driver-pwa/frontend/src/api/driver.test.ts create mode 100644 driver-pwa/frontend/src/api/driver.ts create mode 100644 driver-pwa/frontend/src/store/auth.ts create mode 100644 driver-pwa/frontend/src/vite-env.d.ts diff --git a/driver-pwa/frontend/src/api/client.ts b/driver-pwa/frontend/src/api/client.ts new file mode 100644 index 0000000..6e30f47 --- /dev/null +++ b/driver-pwa/frontend/src/api/client.ts @@ -0,0 +1,24 @@ +import ky, { type KyInstance } from "ky"; +import { useAuth } from "@/store/auth"; + +const API_BASE = import.meta.env.VITE_API_BASE ?? "https://crm.pptaxi.ru/api"; + +export const api: KyInstance = ky.create({ + prefixUrl: API_BASE, + timeout: 30_000, + retry: { limit: 2, methods: ["get"] }, + hooks: { + beforeRequest: [ + (request) => { + const token = useAuth.getState().token; + if (token) request.headers.set("Authorization", `Bearer ${token}`); + }, + ], + beforeError: [ + (error) => { + if (error.response?.status === 401) useAuth.getState().clear(); + return error; + }, + ], + }, +}); diff --git a/driver-pwa/frontend/src/api/driver.test.ts b/driver-pwa/frontend/src/api/driver.test.ts new file mode 100644 index 0000000..9f38c89 --- /dev/null +++ b/driver-pwa/frontend/src/api/driver.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { BalanceSchema, TopupSchema, PaymentsSchema, AuthRequestSchema } from "./driver"; + +describe("driver api schemas", () => { + it("parses balance", () => { + const v = BalanceSchema.parse({ accounts: [{ bucket: "Долг аренда", balance: -3200 }] }); + expect(v.accounts[0].bucket).toBe("Долг аренда"); + }); + it("parses topup", () => { + const v = TopupSchema.parse({ order_id: "o1", pay_url: "https://x", commission: 52, total: 1052 }); + expect(v.pay_url).toBe("https://x"); + }); + it("parses auth request onboarding + code_sent", () => { + expect(AuthRequestSchema.parse({ status: "code_sent", channel: "tg" }).status).toBe("code_sent"); + const o = AuthRequestSchema.parse({ status: "onboarding", deep_links: { tg: "t", max: "m" } }); + expect(o.status === "onboarding" && o.deep_links.tg).toBe("t"); + }); + it("parses payments history", () => { + const v = PaymentsSchema.parse({ payments: [{ order_id: "o1", bucket: "b", amount: 100, + commission: 5.2, method: "qr", status: "PAID", created_at: "2026-06-18T09:00:00Z", paid_at: null }] }); + expect(v.payments[0].status).toBe("PAID"); + }); +}); diff --git a/driver-pwa/frontend/src/api/driver.ts b/driver-pwa/frontend/src/api/driver.ts new file mode 100644 index 0000000..68dc072 --- /dev/null +++ b/driver-pwa/frontend/src/api/driver.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { api } from "./client"; + +export const AuthRequestSchema = z.union([ + z.object({ status: z.literal("code_sent"), channel: z.string() }), + z.object({ status: z.literal("onboarding"), deep_links: z.object({ tg: z.string(), max: z.string() }) }), +]); +export type AuthRequest = z.infer; + +export const VerifySchema = z.object({ token: z.string(), driver_id: z.number() }); +export const BalanceSchema = z.object({ + accounts: z.array(z.object({ bucket: z.string(), balance: z.number() })), +}); +export const TopupSchema = z.object({ + order_id: z.string(), pay_url: z.string(), commission: z.number(), total: z.number(), +}); +export const PaymentStateSchema = z.object({ order_id: z.string(), status: z.string() }); +export const PaymentsSchema = z.object({ + payments: z.array(z.object({ + order_id: z.string(), bucket: z.string(), amount: z.number(), commission: z.number(), + method: z.string(), status: z.string(), + created_at: z.string().nullable(), paid_at: z.string().nullable(), + })), +}); + +export const authRequest = async (phone: string) => + AuthRequestSchema.parse(await api.post("driver/auth/request", { json: { phone } }).json()); +export const authVerify = async (phone: string, code: string) => + VerifySchema.parse(await api.post("driver/auth/verify", { json: { phone, code } }).json()); +export const getBalance = async () => + BalanceSchema.parse(await api.get("driver/balance").json()); +export const topup = async (bucket: string, amount: number, method: "qr" | "card") => + TopupSchema.parse(await api.post("driver/topup", { json: { bucket, amount, method } }).json()); +export const getPaymentState = async (orderId: string) => + PaymentStateSchema.parse(await api.get(`driver/payment/${orderId}`).json()); +export const getPayments = async () => + PaymentsSchema.parse(await api.get("driver/payments").json()); diff --git a/driver-pwa/frontend/src/store/auth.ts b/driver-pwa/frontend/src/store/auth.ts new file mode 100644 index 0000000..f71fd5f --- /dev/null +++ b/driver-pwa/frontend/src/store/auth.ts @@ -0,0 +1,21 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface AuthState { + token: string | null; + driverId: number | null; + setSession: (token: string, driverId: number) => void; + clear: () => void; +} + +export const useAuth = create()( + persist( + (set) => ({ + token: null, + driverId: null, + setSession: (token, driverId) => set({ token, driverId }), + clear: () => set({ token: null, driverId: null }), + }), + { name: "pp-driver-auth" } + ) +); diff --git a/driver-pwa/frontend/src/vite-env.d.ts b/driver-pwa/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..7a7f1cd --- /dev/null +++ b/driver-pwa/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}