190 lines
5.7 KiB
JavaScript
190 lines
5.7 KiB
JavaScript
// 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");
|
||
}
|
||
|
||
// Picks where to send the user after a successful submission, based on platform.
|
||
function getDownloadRedirectUrl() {
|
||
const ua = navigator.userAgent || "";
|
||
|
||
if (/android/i.test(ua)) {
|
||
return "https://myket.ir/app/com.adv.winsoo";
|
||
}
|
||
if (/iphone|ipad|ipod/i.test(ua)) {
|
||
return "https://web.winsoo.ir";
|
||
}
|
||
return "https://winsoo.ir/#Download";
|
||
}
|
||
|
||
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));
|
||
});
|
||
}
|
||
|
||
// 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}`,
|
||
},
|
||
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(body?.status_message || `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");
|
||
setButtonLoading(true, "در حال انتقال...");
|
||
setTimeout(() => {
|
||
window.location.href = getDownloadRedirectUrl();
|
||
}, 1200);
|
||
return;
|
||
} catch (err) {
|
||
console.error(err);
|
||
setMessage(err.message || "خطا در ارسال اطلاعات. لطفاً دوباره تلاش کنید.", "error");
|
||
}
|
||
|
||
setButtonLoading(false);
|
||
}
|
||
|
||
submitBtn.addEventListener("click", handleSubmit);
|
||
|
||
ensureAccessToken().catch((err) => {
|
||
console.error("Failed to fetch access token on load:", err);
|
||
});
|