"use client"; /** * Browser-side calls straight to Gooyal accounts (CORS is open there). * The client_id/secret are intentionally public here — see .env.local comment. */ import { toLatinDigits } from "@/lib/format/persian-digits"; const ACCOUNTS_BASE_URL = process.env.NEXT_PUBLIC_GOOYAL_ACCOUNTS_BASE_URL!; const CLIENT_ID = process.env.NEXT_PUBLIC_GOOYAL_CLIENT_ID!; const CLIENT_SECRET = process.env.NEXT_PUBLIC_GOOYAL_CLIENT_SECRET!; const SCOPE = process.env.NEXT_PUBLIC_GOOYAL_OAUTH_SCOPE!; function basicAuthHeader() { return "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`); } const ERROR_MESSAGES_FA: Record = { invalid_grant: "کد تایید نامعتبر یا منقضی شده است.", invalid_client: "خطا در پیکربندی سرویس احراز هویت. لطفاً بعداً تلاش کنید.", invalid_request: "درخواست نامعتبر است.", }; export class GooyalClientError extends Error { constructor( public status: number, public code: string, description?: string, ) { super(ERROR_MESSAGES_FA[code] ?? description ?? code); } } /** * Accepts local Iranian input (09..., 9..., 0098...) — including Persian-digit * keyboard input — and normalizes to Gooyal's required +989999999999. */ export function normalizeIranianPhone(input: string): string { const digits = toLatinDigits(input).replace(/\D/g, ""); const local = digits.replace(/^0098/, "").replace(/^98/, "").replace(/^0/, ""); return `+98${local}`; } function extractWrappedErrorMessage(data: unknown): string | undefined { const details = (data as { details?: { message?: string | Record } })?.details; if (!details) return undefined; if (typeof details.message === "string") return details.message; if (details.message && typeof details.message === "object") { const first = Object.values(details.message)[0]; return Array.isArray(first) ? first[0] : undefined; } return undefined; } export async function requestOtp(phoneNumber: string) { const res = await fetch(`${ACCOUNTS_BASE_URL}/users/api/request_otp/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone_number: phoneNumber }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new GooyalClientError(res.status, "request_otp_failed", extractWrappedErrorMessage(data)); } return data as { phone_number: string; otp_expire: string; ttl: number }; } export interface GooyalTokenResponse { access_token: string; refresh_token?: string; token_type: string; expires_in: number; scope: string; } export async function exchangeOtpForToken(phoneNumber: string, otp: string) { const body = new URLSearchParams({ grant_type: "password", username: phoneNumber, password: otp, scope: SCOPE, auth_fields: "phone_number:otp", }); const res = await fetch(`${ACCOUNTS_BASE_URL}/oauth2/token/`, { method: "POST", headers: { Authorization: basicAuthHeader(), "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new GooyalClientError(res.status, data.error ?? "token_exchange_failed", data.error_description); } return data as GooyalTokenResponse; }