feat(driver-pwa): auth store + ky client + typed driver API (zod)
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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());
|
||||||
@@ -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
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user