Most affordable and easy-to-use platform in Asia 🌏
Built for sales and call teams
Call customers worldwide from your browser.
No more calling apps that don’t work here, drop your calls, or need a tech expert to set up. We set everything up for you. Your team just logs in, dials, records and follows up, all in one place.
No.Dialsyx runs in your browser. Log in and start calling.
Will my dialer get blocked like other apps?
No.Dialsyx is our own dialer. It is not a third-party app like Google Voice or OpenPhone, so you will not see messages like “your IP doesn’t match” or “you are not in the US, why are you calling US customers?”. Your team logs in and calls.
Can my account get blocked?
No.If you run a legitimate, proper business, nothing will disturb your account during your 30 days or whatever plan you have chosen. We only step in if the calls are used for spam, fraud or anything illegal.
Do I need a VPN or a US IP address?
No.Log in from wherever you are, at the office or from home. There are no location checks on our side that stop your team from calling.
Can I use it on my phone?
Yes.It works in your phone’s browser too, so your team can call from a laptop or a phone.
Can my whole team use it?
Yes.Every agent gets their own login, and you can add more agents later.
Do I need technical knowledge?
No.We set up your account and give you your access. If you can use WhatsApp, you can use Dialsyx.
Can I record my calls?
Yes.Switch recording on and every call is saved, so you can play it back any time. Please follow the call-recording rules in your area.
Can I upload my list of leads?
Yes.Upload a spreadsheet or paste your leads in, then call them one by one.
Can I call customers in other countries?
Yes.Dialsyx is built for teams calling customers around the world. Tell us which countries you need and we will confirm on WhatsApp.
Is there a limit on minutes?
No.Every plan includes unlimited monthly minutes.
Can I pay month to month?
Yes.Choose a monthly plan, or save more with 3, 6 or 12 months.
Will you reject my card or payment?
No.We don’t turn customers down because of where they live. You can pay by bank transfer or card, and we will guide you on WhatsApp.
Can I see how it works before I decide?
Yes.Watch the tour on this page, or message us on WhatsApp and we will walk you through it.
Is there a discount right now?
Yes.New customers get 38% off plus free setup when they start today.
Will I get help if something goes wrong?
Yes.Message us on WhatsApp and a real person will help you. Our support team is available 5 PM to 4 AM Pakistan time (12:00 to 23:00 UTC).
Something else on your mind? Just ask us on WhatsApp.
').join("");
paint();
}
function paint() {
const o = cfg.options.find(x => String(x.value) === String(value));
cur.innerHTML = o ? item(o, false) : '' + (cfg.placeholder || "Choose") + '';
ul.querySelectorAll("li").forEach(li => li.setAttribute("aria-selected", String(li.dataset.v === String(value))));
}
function setAct(i) {
const lis = ul.querySelectorAll("li"); if (!lis.length) return;
act = Math.max(0, Math.min(lis.length - 1, i));
lis.forEach((li, k) => li.classList.toggle("act", k === act));
ul.setAttribute("aria-activedescendant", lis[act].id);
lis[act].scrollIntoView({ block: "nearest" });
}
function open() {
if (isOpen) return; isOpen = true; ul.hidden = false; root.classList.add("open"); btn.setAttribute("aria-expanded", "true");
const sel = cfg.options.findIndex(x => String(x.value) === String(value)); setAct(sel >= 0 ? sel : 0);
}
function close(focus) { if (!isOpen) return; isOpen = false; ul.hidden = true; root.classList.remove("open"); btn.setAttribute("aria-expanded", "false"); if (focus) btn.focus(); }
function choose(i) { const o = cfg.options[i]; if (!o) return; value = o.value; paint(); close(true); cfg.onChange && cfg.onChange(o.value, o); }
btn.addEventListener("click", () => isOpen ? close() : open());
ul.addEventListener("click", e => { const li = e.target.closest("li"); if (li) choose([...ul.children].indexOf(li)); });
ul.addEventListener("mousemove", e => { const li = e.target.closest("li"); if (li) { const i = [...ul.children].indexOf(li); if (i !== act) setAct(i); } });
root.addEventListener("keydown", e => {
const k = e.key;
if (!isOpen) { if (k === "ArrowDown" || k === "ArrowUp" || k === "Enter" || k === " ") { e.preventDefault(); open(); } return; }
if (k === "Escape") { e.preventDefault(); close(true); }
else if (k === "ArrowDown") { e.preventDefault(); setAct(act + 1); }
else if (k === "ArrowUp") { e.preventDefault(); setAct(act - 1); }
else if (k === "Home") { e.preventDefault(); setAct(0); }
else if (k === "End") { e.preventDefault(); setAct(1e6); }
else if (k === "Enter" || k === " ") { e.preventDefault(); choose(act); }
else if (k === "Tab") { close(); }
else if (k.length === 1 && /\S/.test(k)) { // type a letter to jump
buf += k.toLowerCase(); clearTimeout(bufT); bufT = setTimeout(() => { buf = ""; }, 700);
const i = cfg.options.findIndex(o => o.label.replace(/<[^>]+>/g, "").toLowerCase().startsWith(buf)); if (i >= 0) setAct(i);
}
});
document.addEventListener("pointerdown", e => { if (isOpen && !root.contains(e.target)) close(); });
build();
return { set(v) { value = v; paint(); }, setOptions(o) { cfg.options = o; build(); }, get value() { return value; } };
}
/* ============ Pricing calculator ============ */
// All prices are your real prices, in USD, per user per month, BEFORE the offer.
// The crossed-out "was" price is worked out so that 38% off lands exactly on your real price.
const PRICING = {
discount: 0.38, // today's offer
pkrPerUsd: 280, // <-- UPDATE: your rupee rate (how many rupees one dollar is worth). All other currencies use live exchange rates.
teamFrom: 2, // the team rate starts at this many users
extras: { rec: 2, dial: 12 }, // per user per month. The admin portal is included automatically from 2 users, so it has no price
lengths: [ { months: 1, off: 0, label: "Monthly" }, { months: 3, off: 0.10, label: "3 months" }, { months: 6, off: 0.15, label: "6 months" }, { months: 12, off: 0.25, label: "12 months", best: true } ],
users: [1,2,3,4,5,6,7,8,9,10,12,15,20,25,30,40,50],
plans: [
{ id: "us", name: "USA", flags: ["us"], ind: 23, team: 15 },
{ id: "usca", name: "USA + Canada", flags: ["us","ca"], ind: 28, team: 20 },
{ id: "uk", name: "United Kingdom", flags: ["gb"], ind: 30, team: 30 },
{ id: "ukeu", name: "UK to any European country", flags: ["gb","eu"], ind: 35, team: 35, note: "You get a UK number and can call one European country of your choice, like Italy, France, Germany or Cyprus. Unlimited monthly minutes." },
{ id: "au", name: "Australia", flags: ["au"], ind: 45, team: 45 },
{ id: "jp", name: "Japan", flags: ["jp"], ind: 45, team: 45 },
{ id: "global", name: "Global: call anywhere in the world", icon: "globe", ind: 180, team: 180, note: "Includes a US number and unlimited monthly minutes to call anywhere in the world." },
{ id: "other", name: "Another country (ask for a price)", icon: "pin", quote: true }
],
europe: [["al","Albania"],["ad","Andorra"],["at","Austria"],["be","Belgium"],["ba","Bosnia and Herzegovina"],["bg","Bulgaria"],["hr","Croatia"],["cy","Cyprus"],["cz","Czechia"],["dk","Denmark"],["ee","Estonia"],["fi","Finland"],["fr","France"],["de","Germany"],["gr","Greece"],["hu","Hungary"],["is","Iceland"],["ie","Ireland"],["it","Italy"],["xk","Kosovo"],["lv","Latvia"],["li","Liechtenstein"],["lt","Lithuania"],["lu","Luxembourg"],["mt","Malta"],["md","Moldova"],["mc","Monaco"],["me","Montenegro"],["nl","Netherlands"],["mk","North Macedonia"],["no","Norway"],["pl","Poland"],["pt","Portugal"],["ro","Romania"],["sm","San Marino"],["rs","Serbia"],["sk","Slovakia"],["si","Slovenia"],["es","Spain"],["se","Sweden"],["ch","Switzerland"],["ua","Ukraine"]]
};
(function () {
const P = PRICING, $ = id => document.getElementById(id);
const seg = $("pcSeg"), out = $("pcResult");
const st = { plan: "us", users: 3, eu: "", rec: false, dial: false, months: 3, cur: "USD", local: "" };
/* ---------- currencies ---------- */
// "fb" is a backup exchange rate (units per 1 US dollar) used only if live rates cannot be loaded.
// PKR always uses YOUR rate (pkrPerUsd above). Every other currency is an approximate conversion.
const CURRENCIES = {
USD: { n: "US dollar", s: "$", fb: 1 },
PKR: { n: "Pakistani rupee", s: "Rs", sp: 1, fb: 280 },
INR: { n: "Indian rupee", s: "\u20B9", fb: 88 },
CNY: { n: "Chinese yuan", s: "CN\u00A5", fb: 7.1 },
AFN: { n: "Afghan afghani", s: "AFN", sp: 1, fb: 68 },
BDT: { n: "Bangladeshi taka", s: "\u09F3", fb: 122 },
LKR: { n: "Sri Lankan rupee", s: "Rs", sp: 1, fb: 300 },
NPR: { n: "Nepalese rupee", s: "Rs", sp: 1, fb: 140 },
AED: { n: "UAE dirham", s: "AED", sp: 1, fb: 3.6725 },
SAR: { n: "Saudi riyal", s: "SAR", sp: 1, fb: 3.75 },
QAR: { n: "Qatari riyal", s: "QAR", sp: 1, fb: 3.64 },
KWD: { n: "Kuwaiti dinar", s: "KWD", sp: 1, fb: 0.307 },
BHD: { n: "Bahraini dinar", s: "BHD", sp: 1, fb: 0.376 },
OMR: { n: "Omani rial", s: "OMR", sp: 1, fb: 0.385 },
JOD: { n: "Jordanian dinar", s: "JOD", sp: 1, fb: 0.709 },
IQD: { n: "Iraqi dinar", s: "IQD", sp: 1, fb: 1310 },
TRY: { n: "Turkish lira", s: "\u20BA", fb: 42 },
IDR: { n: "Indonesian rupiah", s: "Rp", sp: 1, fb: 16500 },
MYR: { n: "Malaysian ringgit", s: "RM", sp: 1, fb: 4.2 },
SGD: { n: "Singapore dollar", s: "S$", fb: 1.3 },
THB: { n: "Thai baht", s: "\u0E3F", fb: 33 },
VND: { n: "Vietnamese dong", s: "\u20AB", fb: 26300 },
PHP: { n: "Philippine peso", s: "\u20B1", fb: 58 },
JPY: { n: "Japanese yen", s: "JP\u00A5", fb: 150 },
KRW: { n: "South Korean won", s: "\u20A9", fb: 1400 },
HKD: { n: "Hong Kong dollar", s: "HK$", fb: 7.8 },
TWD: { n: "New Taiwan dollar", s: "NT$", fb: 31 },
KZT: { n: "Kazakhstani tenge", s: "\u20B8", fb: 520 },
UZS: { n: "Uzbekistani som", s: "UZS", sp: 1, fb: 12500 },
MMK: { n: "Myanmar kyat", s: "MMK", sp: 1, fb: 2100 },
KHR: { n: "Cambodian riel", s: "KHR", sp: 1, fb: 4050 },
MNT: { n: "Mongolian tugrik", s: "MNT", sp: 1, fb: 3600 }
};
// Which currency to offer first, worked out from the visitor's time zone, then from their browser language.
const TZ_CUR = { "Asia/Karachi": "PKR", "Asia/Kolkata": "INR", "Asia/Calcutta": "INR", "Asia/Shanghai": "CNY", "Asia/Chongqing": "CNY", "Asia/Harbin": "CNY", "Asia/Urumqi": "CNY", "Asia/Kabul": "AFN",
"Asia/Dhaka": "BDT", "Asia/Colombo": "LKR", "Asia/Kathmandu": "NPR", "Asia/Katmandu": "NPR", "Asia/Dubai": "AED", "Asia/Riyadh": "SAR", "Asia/Qatar": "QAR", "Asia/Kuwait": "KWD", "Asia/Bahrain": "BHD",
"Asia/Muscat": "OMR", "Asia/Amman": "JOD", "Asia/Baghdad": "IQD", "Europe/Istanbul": "TRY", "Asia/Istanbul": "TRY", "Asia/Jakarta": "IDR", "Asia/Pontianak": "IDR", "Asia/Makassar": "IDR", "Asia/Jayapura": "IDR",
"Asia/Kuala_Lumpur": "MYR", "Asia/Kuching": "MYR", "Asia/Singapore": "SGD", "Asia/Bangkok": "THB", "Asia/Ho_Chi_Minh": "VND", "Asia/Saigon": "VND", "Asia/Manila": "PHP", "Asia/Tokyo": "JPY", "Asia/Seoul": "KRW",
"Asia/Hong_Kong": "HKD", "Asia/Taipei": "TWD", "Asia/Almaty": "KZT", "Asia/Tashkent": "UZS", "Asia/Samarkand": "UZS", "Asia/Yangon": "MMK", "Asia/Rangoon": "MMK", "Asia/Phnom_Penh": "KHR", "Asia/Ulaanbaatar": "MNT" };
const REGION_CUR = { PK: "PKR", IN: "INR", CN: "CNY", AF: "AFN", BD: "BDT", LK: "LKR", NP: "NPR", AE: "AED", SA: "SAR", QA: "QAR", KW: "KWD", BH: "BHD", OM: "OMR", JO: "JOD", IQ: "IQD", TR: "TRY", ID: "IDR", MY: "MYR",
SG: "SGD", TH: "THB", VN: "VND", PH: "PHP", JP: "JPY", KR: "KRW", HK: "HKD", TW: "TWD", KZ: "KZT", UZ: "UZS", MM: "MMK", KH: "KHR", MN: "MNT" };
function detectLocal() {
try { const q = (new URLSearchParams(location.search).get("cur") || "").toUpperCase(); if (CURRENCIES[q] && q !== "USD") return q; } catch (e) {} // ?cur=INR in an ad link forces a currency
try { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; if (TZ_CUR[tz]) return TZ_CUR[tz]; } catch (e) {}
try { for (const l of (navigator.languages || [navigator.language])) { const r = (l.split("-")[1] || "").toUpperCase(); if (REGION_CUR[r]) return REGION_CUR[r]; } } catch (e) {}
return "";
}
const live = {}; let rateDate = "";
try { const c = JSON.parse(sessionStorage.getItem("dx_rates") || "null"); if (c && Date.now() - c.t < 6 * 3600e3) { Object.assign(live, c.r); rateDate = c.d || ""; } } catch (e) {}
const rateOf = c => c === "USD" ? 1 : c === "PKR" ? P.pkrPerUsd : (live[c] || CURRENCIES[c].fb);
const numStr = (v, d) => v.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
function fmtMoney(usd, c, short) {
const i = CURRENCIES[c], v = usd * rateOf(c);
const d = c === "USD" ? (short && Number.isInteger(usd) ? 0 : 2) : (v >= 100 ? 0 : 2);
return i.s + (i.sp ? " " : "") + numStr(d === 0 ? Math.round(v) : v, d);
}
const money = (usd, c) => fmtMoney(usd, c, false);
const compact = (usd, c) => fmtMoney(usd, c, true);
let ratesLoading = false;
function loadRates() { // live exchange rates (free, no key). If this fails the backup rates above are used.
if (Object.keys(live).length || !window.fetch || ratesLoading) return; ratesLoading = true;
const ctl = window.AbortController ? new AbortController() : null, to = setTimeout(() => ctl && ctl.abort(), 5000);
fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json", ctl ? { signal: ctl.signal } : {})
.then(r => r.json()).then(j => {
const u = j && j.usd; if (!u) return;
Object.keys(CURRENCIES).forEach(c => { const v = u[c.toLowerCase()]; if (c !== "USD" && c !== "PKR" && typeof v === "number" && v > 0) live[c] = v; });
rateDate = j.date || "";
try { sessionStorage.setItem("dx_rates", JSON.stringify({ t: Date.now(), d: rateDate, r: live })); } catch (e) {}
render();
}).catch(() => {}).then(() => { clearTimeout(to); ratesLoading = false; });
}
// start with the visitor's own currency, unless they chose something earlier
st.local = detectLocal();
try { const saved = localStorage.getItem("dx_cur"); if (saved && CURRENCIES[saved]) { st.cur = saved; if (saved !== "USD") st.local = saved; } else st.cur = st.local || "USD"; } catch (e) { st.cur = st.local || "USD"; }
const FLAG = c => '';
const ICON = { globe: '',
pin: '' };
const planIcon = p => p.flags ? p.flags.map(FLAG).join("") : ICON[p.icon] || "";
const planOptions = () => P.plans.map(p => ({ value: p.id, label: p.name, icon: planIcon(p), sub: p.quote ? "" : 'from ' + compact(Math.min(p.ind, p.team), st.cur) }));
const ddPlan = makeDropdown($("ddPlan"), { label: "Where will your team call?", options: planOptions(), value: st.plan, onChange: v => { st.plan = v; if (v === "ukeu") buildEu(); render(); } });
const userOpts = P.users.map(n => ({ value: n, label: n + (n === 1 ? " user" : " users") })).concat([{ value: "more", label: "More than 50 users (ask us)" }]);
const ddUsers = makeDropdown($("ddUsers"), { label: "How many users?", options: userOpts, value: st.users, onChange: v => { st.users = v === "more" ? "more" : Number(v); render(); } });
let ddEu = null;
function buildEu() { // built the first time it is needed, so the 40 flags are not downloaded for nothing
if (ddEu) return;
ddEu = makeDropdown($("ddEu"), { label: "Which European country will you call?", placeholder: "Choose a country", value: st.eu,
options: P.europe.map(([c, n]) => ({ value: c, label: n, icon: FLAG(c) })), onChange: v => { st.eu = v; render(); } });
}
const euName = () => { const e = P.europe.find(x => x[0] === st.eu); return e ? e[1] : ""; };
const planLabel = p => p.id === "ukeu" ? "UK number to " + (euName() || "one European country") : p.name;
P.lengths.forEach(l => {
const b = document.createElement("button"); b.type = "button"; b.dataset.m = l.months; if (l.best) b.className = "best";
b.innerHTML = "" + l.label + "" + (l.off ? "Extra " + Math.round(l.off*100) + "% off" : " ") + "";
seg.appendChild(b);
});
const plan = () => P.plans.find(p => p.id === st.plan);
const len = () => P.lengths.find(l => l.months === st.months);
function calc() {
const p = plan(), users = st.users === "more" ? null : Number(st.users);
if (p.quote || users === null) return { quote: true };
const team = users >= P.teamFrom;
const base = team ? p.team : p.ind;
const extra = (st.rec ? P.extras.rec : 0) + (st.dial ? P.extras.dial : 0);
const pay = base + extra; // today's offer price per user per month
const was = pay / (1 - P.discount); // price before the offer
const L = len(), final = pay * (1 - L.off); // after the plan-length discount
const total = final * users * L.months, wasTotal = was * users * L.months;
return { p, users, team, base, extra, pay, was, L, final, total, wasTotal, save: wasTotal - total };
}
const extrasText = () => [st.rec && "Auto call recording", st.dial && "Auto dialing & campaigns"].filter(Boolean).join(" + ") || "No extras";
function pricingLines(c) {
const L = [];
if (c.quote) {
L.push("I'd like a price for: " + (plan().quote ? "another country" : "a team of more than 50 users"), "Number of users: " + (st.users === "more" ? "more than 50" : st.users), "Extras: " + extrasText());
} else {
L.push("Plan: " + planLabel(c.p), "Users: " + c.users + (c.team ? " (team rate, admin portal included)" : ""), "Extras: " + extrasText(), "Plan length: " + c.L.label,
"Price: " + money(c.final, "USD") + " per user per month (" + Math.round(P.discount*100) + "% off today" + (c.L.off ? " + extra " + Math.round(c.L.off*100) + "% for the " + c.L.months + "-month plan" : "") + ")",
"Total for " + c.L.months + (c.L.months === 1 ? " month" : " months") + ": " + money(c.total, "USD") + (st.cur !== "USD" ? " (about " + money(c.total, st.cur) + ")" : ""));
}
return L;
}
function ctaHref(c) {
return c.quote ? waLink("pricingquote", null, pricingLines(c), "Can you send me the price?")
: waLink("pricing", null, pricingLines(c), "I want to get started. What are the next steps?");
}
let lastCur = st.cur;
function render() {
const c = calc(), cur = st.cur;
// controls
const pl = plan();
$("pcPlanHint").innerHTML = pl.quote ? "Tell us the country and we will send you a price." : pl.note ? pl.note : "Unlimited monthly minutes. Team rate applies from " + P.teamFrom + " users.";
if (lastCur !== cur) { lastCur = cur; ddPlan.setOptions(planOptions()); }
$("pcRec").querySelector(".pc-pr").textContent = "+" + compact(P.extras.rec, cur) + " / user";
$("pcDial").querySelector(".pc-pr").textContent = "+" + compact(P.extras.dial, cur) + " / user";
$("euWrap").hidden = pl.id !== "ukeu"; ddPlan.set(st.plan); ddUsers.set(st.users); if (ddEu) ddEu.set(st.eu);
$("pcUsersHint").innerHTML = (pl.quote || st.users === "more") ? "We will quote you on WhatsApp." :
(Number(st.users) >= P.teamFrom ? "Team rate applied. " + (pl.team < pl.ind ? "You pay " + compact(pl.ind - pl.team, cur) + " less per user than the single-user rate." : "") : "Add a second user to unlock the team rate.");
$("pcRec").setAttribute("aria-pressed", String(st.rec)); $("pcDial").setAttribute("aria-pressed", String(st.dial));
const isTeam = !(pl.quote || st.users === "more") && Number(st.users) >= P.teamFrom;
$("pcExtraHint").innerHTML = (pl.quote || st.users === "more") ? "" : isTeam ? "Admin portal for team management is included because you have " + P.teamFrom + " or more users." : "Add a second user and the admin portal for team management is included.";
seg.querySelectorAll("button").forEach(b => b.setAttribute("aria-pressed", String(Number(b.dataset.m) === st.months)));
const locBtn = $("curLocal"); locBtn.hidden = !st.local; locBtn.textContent = st.local || "";
document.querySelector('[data-cur="USD"]').setAttribute("aria-pressed", String(cur === "USD")); locBtn.setAttribute("aria-pressed", String(!!st.local && cur === st.local));
document.querySelectorAll("#curPop li").forEach(li => li.setAttribute("aria-selected", String(li.dataset.c === cur)));
if (cur !== "USD") loadRates();
// result
if (c.quote) {
$("pcBody").innerHTML = '
Let’s price it for you
' + (pl.quote ? "Tell us which country your team will call and we will send you a price. Today’s " + Math.round(P.discount*100) + "% off applies." : "For teams of more than 50 users we give you a special price. Message us and we will quote you.") + '
Included: unlimited monthly minutes, ' + (c.team ? 'admin portal for team management, ' : '') + 'free setup, calling in your browser, call logs and details, contacts and CRM, follow-ups, scripts and WhatsApp support.
';
}
const rt = rateOf(cur), rtxt = numStr(rt, rt >= 100 ? 0 : 2) + " " + cur;
$("pcNote").textContent = "Prices are per user per month" + (cur === "USD" ? ", in US dollars" : cur === "PKR" ? ", shown in Pakistani rupees at Rs " + P.pkrPerUsd + " per US dollar (the rate can change)"
: ", shown in " + CURRENCIES[cur].n + " (" + cur + ") as an approximate amount at about " + rtxt + " per US dollar" + (live[cur] && rateDate ? " on " + rateDate : "") + ". The final price is confirmed with you on WhatsApp")
+ ". The " + Math.round(P.discount*100) + "% offer price applies to new customers who start today. Need a different country or a bigger team? Just ask us.";
tickTimer();
}
function tickTimer() {
let left = 0; try { left = Math.max(0, Math.floor((endTime - Date.now()) / 1000)); } catch (e) {}
const pad = n => String(n).padStart(2, "0"), t = pad(Math.floor(left/3600)) + ":" + pad(Math.floor(left%3600/60)) + ":" + pad(left%60);
document.querySelectorAll(".pcT").forEach(e => e.textContent = t);
}
setInterval(tickTimer, 1000);
$("pcRec").addEventListener("click", () => { st.rec = !st.rec; render(); });
$("pcDial").addEventListener("click", () => { st.dial = !st.dial; render(); });
seg.addEventListener("click", e => { const b = e.target.closest("button"); if (b) { st.months = Number(b.dataset.m); render(); } });
out.addEventListener("click", e => {
const b = e.target.closest("[data-cur]"); if (b) { chooseCur(b.dataset.cur === "USD" ? "USD" : st.local); return; }
const a = e.target.closest("#pcCta");
if (a && typeof fbq === "function") { const c = calc(); fbq("track", "Contact", { content_name: "whatsapp_pricing", content_category: "WhatsApp", value: c.quote ? 0 : Math.round(c.total*100)/100, currency: "USD" }, { eventID: "dx_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8) }); }
});
// fixed top of the result panel: offer badge + currency switch (USD, the visitor's own currency, and a list of the others)
out.innerHTML = '
' + Math.round(P.discount*100) + '% OFF TODAY' +
'
' +
'
' +
'
' + Object.keys(CURRENCIES).map(c => '
' + c + '' + CURRENCIES[c].n + '
').join("") + '
';
function chooseCur(c) {
st.cur = c; if (c !== "USD") st.local = c;
try { localStorage.setItem("dx_cur", c); } catch (e) {}
closePop(); render();
}
function closePop() { $("curPop").hidden = true; $("curMore").setAttribute("aria-expanded", "false"); }
$("curMore").addEventListener("click", e => { e.stopPropagation(); const h = $("curPop").hidden; $("curPop").hidden = !h; $("curMore").setAttribute("aria-expanded", String(h)); });
$("curPop").addEventListener("click", e => { const li = e.target.closest("li"); if (li) chooseCur(li.dataset.c); });
document.addEventListener("pointerdown", e => { if (!$("curPop").hidden && !e.target.closest(".cur-more")) closePop(); });
document.addEventListener("keydown", e => { if (e.key === "Escape") closePop(); });
if (st.cur !== "USD") loadRates();
render();
window.__pricing = { calc, st, render, lines: () => pricingLines(calc()) }; // lines() is used by the "more countries" link; the rest is for testing
})();
/* ============ Countdown logic ============ */
const DURATION_MS = CONFIG.durationHours * 60 * 60 * 1000;
function safeGet(key) {
try { return window.localStorage.getItem(key); } catch (e) { return null; }
}
function safeSet(key, val) {
try { window.localStorage.setItem(key, val); } catch (e) { /* storage blocked: timer still runs for this visit */ }
}
// Testing helper: add ?resettimer to the URL to restart the 4 hours
if (new URLSearchParams(location.search).has("resettimer")) {
try { window.localStorage.removeItem(CONFIG.storageKey); } catch (e) {}
}
let endTime = parseInt(safeGet(CONFIG.storageKey), 10);
const now = Date.now();
// First visit, or a returning visitor whose window already expired: start a fresh window
if (!endTime || isNaN(endTime) || endTime <= now) {
endTime = now + DURATION_MS;
safeSet(CONFIG.storageKey, String(endTime));
}
const el = {
h1: document.getElementById("h1"), h2: document.getElementById("h2"),
m1: document.getElementById("m1"), m2: document.getElementById("m2"),
s1: document.getElementById("s1"), s2: document.getElementById("s2"),
bar: document.getElementById("offerBar"),
label: document.getElementById("countdownLabel")
};
function render(msLeft) {
const total = Math.max(0, Math.floor(msLeft / 1000));
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
const pad = n => String(n).padStart(2, "0");
const hh = pad(h), mm = pad(m), ss = pad(s);
el.h1.textContent = hh[0]; el.h2.textContent = hh[1];
el.m1.textContent = mm[0]; el.m2.textContent = mm[1];
el.s1.textContent = ss[0]; el.s2.textContent = ss[1];
}
let interval;
function tick() {
const left = endTime - Date.now();
render(left);
if (left <= 0) {
el.bar.classList.add("expired");
el.label.textContent = CONFIG.expiredLabel;
clearInterval(interval);
}
}
render(endTime - Date.now());
interval = setInterval(tick, 1000);
tick();