register-invite-requests-front/js/script.js
2026-08-23 12:59:55 +03:30

186 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// NOTE: staging credentials. Calling an OAuth2 client_credentials flow from
// the browser exposes CLIENT_SECRET to anyone who views the page source.
const AUTH_CONFIG = {
CLIENT_ID: "4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u",
CLIENT_SECRET:
"KPc4dMSztNwAIB3vii3geXrzC1mKUIsAztz3t2ylC3HlrgJudJWhdrtoY6XeRJIuadTAREYYsXk9XtFHUDfPVcJvyfHMpNkz2VvghwrijhVprok0VYV7XrOirJ5nFUxD",
TOKEN_URL: "https://accounts-staging.gooyal.ir/oauth2/token/",
SCOPE: "accounts.invite:submit",
};
const INVITE_URL = "https://accounts-staging.gooyal.ir/users/api/invite_requests/";
const phoneInput = document.getElementById("phoneInput");
const submitBtn = document.getElementById("submitBtn");
const formMessage = document.getElementById("formMessage");
let accessToken = null;
let tokenRequest = null;
function setMessage(text, type) {
formMessage.textContent = text || "";
formMessage.classList.remove("error", "success");
if (type) formMessage.classList.add(type);
}
function setButtonLoading(isLoading, label) {
submitBtn.disabled = isLoading;
submitBtn.textContent = label || "ثبت شماره و دانلود برنامه";
}
async function fetchAccessToken() {
const basicAuth = btoa(`${AUTH_CONFIG.CLIENT_ID}:${AUTH_CONFIG.CLIENT_SECRET}`);
const response = await fetch(AUTH_CONFIG.TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: "client_credentials",
scope: AUTH_CONFIG.SCOPE,
}),
});
if (!response.ok) {
throw new Error(`Token request failed (${response.status})`);
}
const data = await response.json();
accessToken = data.access_token;
return accessToken;
}
// Reuses an in-flight request so concurrent callers don't fire duplicate token requests.
function ensureAccessToken() {
if (accessToken) return Promise.resolve(accessToken);
if (!tokenRequest) {
tokenRequest = fetchAccessToken().finally(() => {
tokenRequest = null;
});
}
return tokenRequest;
}
function getInviteCode() {
return new URLSearchParams(window.location.search).get("inviteCode");
}
const PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹";
const ARABIC_INDIC_DIGITS = "٠١٢٣٤٥٦٧٨٩";
// Converts Persian (۰-۹) and Arabic-Indic (٠-٩) numerals to Latin digits.
function toLatinDigits(str) {
return str.replace(/[۰-۹٠-٩]/g, (char) => {
const persianIndex = PERSIAN_DIGITS.indexOf(char);
if (persianIndex !== -1) return String(persianIndex);
return String(ARABIC_INDIC_DIGITS.indexOf(char));
});
}
function getApiErrorMessage(body, fallback) {
const detailsMessage = body?.details?.message;
if (typeof detailsMessage === "string" && detailsMessage.trim()) {
return detailsMessage.trim();
}
if (typeof body?.status_message === "string" && body.status_message.trim()) {
return body.status_message.trim();
}
return fallback;
}
// Normalizes Iranian mobile numbers (09xxxxxxxxx, 9xxxxxxxxx, 0098/98/+98 prefixed) to E.164.
function toE164(raw) {
let cleaned = toLatinDigits(raw.trim()).replace(/[\s-]/g, "");
if (cleaned.startsWith("+98")) cleaned = cleaned.slice(3);
else if (cleaned.startsWith("0098")) cleaned = cleaned.slice(4);
else if (cleaned.startsWith("98")) cleaned = cleaned.slice(2);
else if (cleaned.startsWith("0")) cleaned = cleaned.slice(1);
if (!/^9\d{9}$/.test(cleaned)) return null;
return `+98${cleaned}`;
}
async function submitInviteRequest(inviteCode, mobileNumber, token) {
const response = await fetch(INVITE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"Accept-Language": "fa",
},
body: JSON.stringify({
invite_code: inviteCode,
mobile_number: mobileNumber,
}),
});
if (!response.ok) {
let body = null;
try {
body = await response.json();
} catch (_) {
// no JSON body
}
const error = new Error(
getApiErrorMessage(body, `Invite request failed (${response.status})`)
);
error.status = response.status;
error.body = body;
throw error;
}
return response.json();
}
async function handleSubmit() {
phoneInput.classList.remove("invalid");
setMessage("", null);
const inviteCode = getInviteCode();
if (!inviteCode) {
setMessage("لینک دعوت نامعتبر است.", "error");
return;
}
const mobileNumber = toE164(phoneInput.value);
if (!mobileNumber) {
phoneInput.classList.add("invalid");
setMessage("شماره موبایل را به‌درستی وارد کنید.", "error");
return;
}
setButtonLoading(true, "در حال ارسال...");
try {
const token = await ensureAccessToken();
try {
await submitInviteRequest(inviteCode, mobileNumber, token);
} catch (err) {
// Token may have expired between page load and submit — retry once with a fresh one.
if (err.status === 401) {
accessToken = null;
const freshToken = await ensureAccessToken();
await submitInviteRequest(inviteCode, mobileNumber, freshToken);
} else {
throw err;
}
}
setMessage("ثبت شماره با موفقیت انجام شد.", "success");
} catch (err) {
console.error(err);
setMessage(err.message || "خطا در ارسال اطلاعات. لطفاً دوباره تلاش کنید.", "error");
} finally {
setButtonLoading(false);
}
}
submitBtn.addEventListener("click", handleSubmit);
ensureAccessToken().catch((err) => {
console.error("Failed to fetch access token on load:", err);
});