diff --git a/.info/openapi.json b/.info/openapi.json deleted file mode 100644 index 2e38d7c..0000000 --- a/.info/openapi.json +++ /dev/null @@ -1 +0,0 @@ -{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/auth/signup":{"post":{"summary":"Signup","operationId":"signup_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegistration"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"post":{"summary":"Login","operationId":"login_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLoginData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/refresh":{"post":{"summary":"Refresh","operationId":"refresh_auth_refresh_post","parameters":[{"name":"refresh_token","in":"query","required":true,"schema":{"type":"string","title":"Refresh Token"}},{"name":"iss","in":"query","required":true,"schema":{"enum":["credentials","telegram","api"],"type":"string","title":"Iss"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserTokens"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/plans/":{"get":{"summary":"Get Plans","operationId":"get_plans_plans__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingPlans"}}}}}}},"/orders/checkout":{"post":{"summary":"Checkout","operationId":"checkout_orders_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDetails"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/users/me":{"get":{"summary":"Get Me","operationId":"get_me_users_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}}}}},"/users/subscription":{"get":{"summary":"Get Subscription","operationId":"get_subscription_users_subscription_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/users/subscription/hwid":{"get":{"summary":"Get Hwid","operationId":"get_hwid_users_subscription_hwid_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Device"},"title":"Response Get Hwid Users Subscription Hwid Get"}}}}}},"delete":{"summary":"Delete Hwid Endpoint","operationId":"delete_hwid_endpoint_users_subscription_hwid_delete","parameters":[{"name":"hwid","in":"query","required":true,"schema":{"type":"string","title":"Hwid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payments/pally/result":{"post":{"summary":"Pally Callback","operationId":"pally_callback_payments_pally_result_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_pally_callback_payments_pally_result_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddonData":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price"},"free_threshold":{"type":"integer","title":"Free Threshold"}},"type":"object","required":["id","name","price","free_threshold"],"title":"AddonData"},"Body_pally_callback_payments_pally_result_post":{"properties":{"InvId":{"type":"string","title":"Invid"},"OutSum":{"type":"string","title":"Outsum"},"Commission":{"type":"string","title":"Commission"},"TrsId":{"type":"string","title":"Trsid"},"Status":{"type":"string","title":"Status"},"CurrencyIn":{"type":"string","title":"Currencyin"},"custom":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom"},"SignatureValue":{"type":"string","title":"Signaturevalue"},"AccountType":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accounttype"},"AccountNumber":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accountnumber"},"BalanceAmount":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balanceamount"},"BalanceCurrency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balancecurrency"},"PayerPhone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payerphone"},"PayerEmail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payeremail"},"PayerName":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payername"},"PayerComment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payercomment"},"ErrorCode":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Errorcode"},"ErrorMessage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Errormessage"}},"type":"object","required":["InvId","OutSum","Commission","TrsId","Status","CurrencyIn","SignatureValue"],"title":"Body_pally_callback_payments_pally_result_post"},"CheckoutResponse":{"properties":{"order_id":{"type":"string","title":"Order Id"},"total_amount":{"type":"number","title":"Total Amount"},"bonus_paid":{"type":"number","title":"Bonus Paid"},"amount_to_pay":{"type":"number","title":"Amount To Pay"},"payment_link":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Link"}},"type":"object","required":["order_id","total_amount","bonus_paid","amount_to_pay"],"title":"CheckoutResponse"},"Device":{"properties":{"os":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"client":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client"},"hwid":{"type":"string","title":"Hwid"}},"type":"object","required":["hwid"],"title":"Device"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"OrderDetails":{"properties":{"devices":{"type":"integer","title":"Devices"},"addons":{"items":{"type":"string"},"type":"array","title":"Addons","description":"List of addons ids"},"duration_days":{"type":"integer","title":"Duration Days"}},"type":"object","required":["devices","addons","duration_days"],"title":"OrderDetails"},"PricingPlans":{"properties":{"device_price":{"type":"integer","title":"Device Price"},"addons":{"items":{"$ref":"#/components/schemas/AddonData"},"type":"array","title":"Addons"}},"type":"object","required":["device_price","addons"],"title":"PricingPlans"},"TelegramData":{"properties":{},"type":"object","title":"TelegramData"},"UserInfo":{"properties":{"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"referal_code":{"type":"string","title":"Referal Code"},"bonus_balance":{"type":"number","title":"Bonus Balance"}},"type":"object","required":["referal_code","bonus_balance"],"title":"UserInfo"},"UserLogin":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"expires_at":{"type":"number","title":"Expires At"},"user":{"$ref":"#/components/schemas/UserInfo"}},"type":"object","required":["access_token","refresh_token","expires_at","user"],"title":"UserLogin"},"UserLoginData":{"properties":{"provider":{"type":"string","enum":["credentials","telegram","api"],"title":"Provider"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"telegram":{"anyOf":[{"$ref":"#/components/schemas/TelegramData"},{"type":"null"}]}},"type":"object","required":["provider"],"title":"UserLoginData"},"UserRegistration":{"properties":{"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Username"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"referal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Referal Code"},"provider":{"type":"string","enum":["credentials","telegram","api"],"title":"Provider"}},"type":"object","required":["provider"],"title":"UserRegistration"},"UserTokens":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"expires_at":{"type":"number","title":"Expires At"}},"type":"object","required":["access_token","refresh_token","expires_at"],"title":"UserTokens"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}} \ No newline at end of file diff --git a/src/api/client.ts b/src/api/client.ts index 2630ce3..6c2f8f1 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,10 +1,10 @@ import { useAuthStore } from '../store/useStore'; import type { + AuthenticatedUser, CheckoutResponse, Device, LinkCodeResponse, LoginPayload, - LoginResponse, OperationResult, OrderDetails, PricingPlans, @@ -155,11 +155,11 @@ async function request(path: string, opts: RequestOptions = {}): Promise { /* ── Public API ───────────────────────────────────────────── */ export const api = { - signup(payload: SignupPayload): Promise { - return request('/api/auth/signup', { method: 'POST', body: payload, auth: false }); + signup(payload: SignupPayload): Promise { + return request('/api/auth/signup', { method: 'POST', body: payload, auth: false }); }, - login(payload: LoginPayload): Promise { - return request('/api/auth/login', { method: 'POST', body: payload, auth: false }); + login(payload: LoginPayload): Promise { + return request('/api/auth/login', { method: 'POST', body: payload, auth: false }); }, getPlans(): Promise { return request('/api/plans/', { auth: false }); diff --git a/src/components/DeviceSlider.tsx b/src/components/DeviceSlider.tsx index 2afcfe8..8005b41 100644 --- a/src/components/DeviceSlider.tsx +++ b/src/components/DeviceSlider.tsx @@ -117,7 +117,7 @@ export function DeviceSlider({ min, max, value, onChange }: DeviceSliderProps) {
Устройства - {value}  мест + {value}  устройств
diff --git a/src/components/StatStrip.tsx b/src/components/StatStrip.tsx index b54c2a5..4b88a1d 100644 --- a/src/components/StatStrip.tsx +++ b/src/components/StatStrip.tsx @@ -32,6 +32,7 @@ export function StatStrip({ user, subscription, deviceCount }: StatStripProps) { const telegramId = user?.telegram_id ?? null; const devicesLimit = subscription?.devices ?? null; const expiresAt = subscription?.expires_at ?? null; + const isExpired = expiresAt !== null && new Date(expiresAt).getTime() < Date.now(); const bonusBalance = user?.bonus_balance ?? 0; return ( @@ -57,7 +58,7 @@ export function StatStrip({ user, subscription, deviceCount }: StatStripProps) { } label="Telegram ID" > - {telegramId ? @{telegramId} : не привязан} + {telegramId ? {telegramId} : не привязан} } - label="Продление" + label="Подписка до" > - {expiresAt ? {formatDate(expiresAt)} : —} + {expiresAt ? ( + {isExpired ? 'Истекла' : formatDate(expiresAt)} + ) : ( + — + )} { if (event.target === event.currentTarget) onClose(); }} >
-
{label}
+
Сканировать

- Откройте на устройстве + {label}

-

Отсканируйте код камерой или приложением Malenia.

{image ? {imageAlt} : Генерируем QR-код}
diff --git a/src/pages/AccountPage.test.tsx b/src/pages/AccountPage.test.tsx index 73395d3..c593348 100644 --- a/src/pages/AccountPage.test.tsx +++ b/src/pages/AccountPage.test.tsx @@ -42,7 +42,7 @@ function renderAccount(authed = true) { access: 'a', refresh: 'r', expiresAt: 9999999999, - user: { username: 'alice', telegram_id: 'alice_id', referal_code: 'c', bonus_balance: 150 }, + user: { username: 'alice', telegram_id: 123456789, referal_code: 'c', bonus_balance: 150 }, remember: true, }); } else { @@ -71,11 +71,11 @@ function PlanLocation() { return
{`plan page${location.search}`}
; } -const ME = { username: 'alice', telegram_id: 'alice_id', referal_code: 'c', bonus_balance: 150 }; +const ME = { username: 'alice', telegram_id: 123456789, referal_code: 'c', bonus_balance: 150 }; const SUB = { devices: 6, duration_days: 90, - expires_at: '2026-03-14T00:00:00Z', + expires_at: '2099-03-14T00:00:00Z', addon_ids: [], subscription_link: 'link', has_subscription: true, @@ -107,7 +107,7 @@ describe('AccountPage', () => { vi.mocked(api.getDevices).mockResolvedValue(DEVICES); renderAccount(); expect(await screen.findByText('alice')).toBeInTheDocument(); - expect(screen.getByText('@alice_id')).toBeInTheDocument(); + expect(screen.getByText('123456789')).toBeInTheDocument(); expect(screen.getByText(/150\s*₽/)).toBeInTheDocument(); expect(screen.getByText(/только на подписку/i)).toBeInTheDocument(); expect(screen.getAllByText(/iPhone 15/)).toHaveLength(1); @@ -120,7 +120,17 @@ describe('AccountPage', () => { renderAccount(); expect(screen.queryByText(/активная подписка отсутствует/i)).not.toBeInTheDocument(); - expect(await screen.findByText(/подписка до/i)).toBeInTheDocument(); + expect(await screen.findAllByText(/подписка до/i)).toHaveLength(2); + }); + + it('renders expired subscription status', async () => { + vi.mocked(api.getMe).mockResolvedValue(ME); + vi.mocked(api.getSubscription).mockResolvedValue({ ...SUB, expires_at: '2020-01-01T00:00:00Z' }); + vi.mocked(api.getDevices).mockResolvedValue(DEVICES); + renderAccount(); + + expect(await screen.findAllByText('Истекла')).toHaveLength(2); + expect(screen.queryByText(/^Подписка до \d/)).not.toBeInTheDocument(); }); it('renders the subscription link and copy action', async () => { diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx index 7451761..1784a58 100644 --- a/src/pages/AccountPage.tsx +++ b/src/pages/AccountPage.tsx @@ -159,11 +159,13 @@ export function AccountPage() {

{userInfo?.username ?? 'Аккаунт'}

{!loadingMeta ? ( <> -

- {subscription?.has_subscription && subscription.expires_at - ? `Подписка до ${new Date(subscription.expires_at).toLocaleDateString('ru-RU')}` - : 'Активная подписка отсутствует'} -

+

+ {subscription?.has_subscription && subscription.expires_at + ? new Date(subscription.expires_at).getTime() < Date.now() + ? 'Истекла' + : `Подписка до ${new Date(subscription.expires_at).toLocaleDateString('ru-RU')}` + : 'Активная подписка отсутствует'} +

{subscription?.has_subscription ? (