feat(driver-pwa): auth store + ky client + typed driver API (zod)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-18 19:17:12 +10:00
co-authored by claude-flow
parent 34d3554f72
commit a7032692f1
5 changed files with 114 additions and 0 deletions
+24
View File
@@ -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;
},
],
},
});
@@ -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");
});
});
+37
View File
@@ -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<typeof AuthRequestSchema>;
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());
+21
View File
@@ -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<AuthState>()(
persist(
(set) => ({
token: null,
driverId: null,
setSession: (token, driverId) => set({ token, driverId }),
clear: () => set({ token: null, driverId: null }),
}),
{ name: "pp-driver-auth" }
)
);
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}