diff --git a/h5/activity/recharge-agency-list/index.html b/h5/activity/recharge-agency-list/index.html new file mode 100644 index 0000000..9042d54 --- /dev/null +++ b/h5/activity/recharge-agency-list/index.html @@ -0,0 +1,171 @@ + + + + + + + Recharge Agency + + + + +
+
+

Recharge Agency

+ +
+ + + +
+ + Recharge Agency + +
+ + +
+
+ + + diff --git a/h5/activity/recharge/index.html b/h5/activity/recharge/index.html new file mode 100644 index 0000000..7de5301 --- /dev/null +++ b/h5/activity/recharge/index.html @@ -0,0 +1,677 @@ + + + + + + + Recharge Reward + + + + +
+
+

Recharge Reward

+ +
+
+ +
+ Recharge Reward +
+ + + +
+ +
01
+
Days
+
+
+ +
01
+
Hours
+
+
+ +
01
+
Minutes
+
+
+ +
01
+
Seconds
+
+ +
+ +
+ +
+ + + + +
+ +
+ +
+ + +
nanfangjian
+
ID:12345678
+
Accumlated Recharge:5998
+ +
+ + +
Recharge $100
+ + + + + + +
+ + $100 +
+
+ + $100 +
+
+ +
+ + +
Recharge $100
+ + + + + + + + +
+ + $100 +
+
+ + $100 +
+
+ + $100 +
+
+
+
+ + + + + diff --git a/h5/app-invite/app.js b/h5/app-invite/app.js index 43e241e..fb5f17a 100644 --- a/h5/app-invite/app.js +++ b/h5/app-invite/app.js @@ -729,7 +729,7 @@ function renderRankRows(container) {
No ranking data yet
-
0人
+
0 person
0
` @@ -946,7 +946,7 @@ function normalizeRankList(list) { rank, name: userName, avatar: trimValue(item?.avatar) || DEFAULT_AVATAR, - inviteCountText: `${formatMetric(inviteCount)}人`, + inviteCountText: `${formatMetric(inviteCount)} person`, rewardText: formatMetric(rewardGoldCoins), score: scoreParts.join(" · "), }; diff --git a/h5/assets/recharge-agency-list/bottom-frame.svg b/h5/assets/recharge-agency-list/bottom-frame.svg new file mode 100644 index 0000000..b18972f --- /dev/null +++ b/h5/assets/recharge-agency-list/bottom-frame.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/h5/assets/recharge-agency-list/mask.svg b/h5/assets/recharge-agency-list/mask.svg new file mode 100644 index 0000000..c436e8a --- /dev/null +++ b/h5/assets/recharge-agency-list/mask.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/h5/assets/recharge-agency-list/palace-hall.png b/h5/assets/recharge-agency-list/palace-hall.png new file mode 100644 index 0000000..c69b2b3 Binary files /dev/null and b/h5/assets/recharge-agency-list/palace-hall.png differ diff --git a/h5/assets/recharge-agency-list/title-figure.png b/h5/assets/recharge-agency-list/title-figure.png new file mode 100644 index 0000000..d8c48ca Binary files /dev/null and b/h5/assets/recharge-agency-list/title-figure.png differ diff --git a/h5/assets/recharge-agency-list/title-ribbon.png b/h5/assets/recharge-agency-list/title-ribbon.png new file mode 100644 index 0000000..bb317f9 Binary files /dev/null and b/h5/assets/recharge-agency-list/title-ribbon.png differ diff --git a/h5/assets/recharge-agency-list/top-glow.png b/h5/assets/recharge-agency-list/top-glow.png new file mode 100644 index 0000000..6702cc0 Binary files /dev/null and b/h5/assets/recharge-agency-list/top-glow.png differ diff --git a/h5/assets/recharge/avatar-frame-mask-301.png b/h5/assets/recharge/avatar-frame-mask-301.png new file mode 100644 index 0000000..e3e56e4 --- /dev/null +++ b/h5/assets/recharge/avatar-frame-mask-301.png @@ -0,0 +1,3 @@ + + + diff --git a/h5/assets/recharge/avatar-photo.png b/h5/assets/recharge/avatar-photo.png new file mode 100644 index 0000000..6d0410b Binary files /dev/null and b/h5/assets/recharge/avatar-photo.png differ diff --git a/h5/assets/recharge/countdown-box.svg b/h5/assets/recharge/countdown-box.svg new file mode 100644 index 0000000..e3a05f3 --- /dev/null +++ b/h5/assets/recharge/countdown-box.svg @@ -0,0 +1,12 @@ + +
+ + + + + + + + + +
diff --git a/h5/assets/recharge/hero-bg.png b/h5/assets/recharge/hero-bg.png new file mode 100644 index 0000000..fedfff4 Binary files /dev/null and b/h5/assets/recharge/hero-bg.png differ diff --git a/h5/assets/recharge/hero-mask.png b/h5/assets/recharge/hero-mask.png new file mode 100644 index 0000000..ea8a647 --- /dev/null +++ b/h5/assets/recharge/hero-mask.png @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/h5/assets/recharge/hero-title.png b/h5/assets/recharge/hero-title.png new file mode 100644 index 0000000..8e02ec3 Binary files /dev/null and b/h5/assets/recharge/hero-title.png differ diff --git a/h5/assets/recharge/panel-edge.png b/h5/assets/recharge/panel-edge.png new file mode 100644 index 0000000..7908690 Binary files /dev/null and b/h5/assets/recharge/panel-edge.png differ diff --git a/h5/assets/recharge/panel-middle.png b/h5/assets/recharge/panel-middle.png new file mode 100644 index 0000000..5efce4a Binary files /dev/null and b/h5/assets/recharge/panel-middle.png differ diff --git a/h5/assets/recharge/price-button-fill.png b/h5/assets/recharge/price-button-fill.png new file mode 100644 index 0000000..54e825c Binary files /dev/null and b/h5/assets/recharge/price-button-fill.png differ diff --git a/h5/assets/recharge/price-button-mask.png b/h5/assets/recharge/price-button-mask.png new file mode 100644 index 0000000..c568f74 --- /dev/null +++ b/h5/assets/recharge/price-button-mask.png @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/h5/assets/recharge/profile-panel-bg.png b/h5/assets/recharge/profile-panel-bg.png new file mode 100644 index 0000000..29dda3e Binary files /dev/null and b/h5/assets/recharge/profile-panel-bg.png differ diff --git a/h5/assets/recharge/reward-frame-center.png b/h5/assets/recharge/reward-frame-center.png new file mode 100644 index 0000000..5ea5d9a Binary files /dev/null and b/h5/assets/recharge/reward-frame-center.png differ diff --git a/h5/assets/recharge/reward-frame-corners.png b/h5/assets/recharge/reward-frame-corners.png new file mode 100644 index 0000000..862251d Binary files /dev/null and b/h5/assets/recharge/reward-frame-corners.png differ diff --git a/h5/assets/recharge/reward-frame-fill.png b/h5/assets/recharge/reward-frame-fill.png new file mode 100644 index 0000000..a544aac Binary files /dev/null and b/h5/assets/recharge/reward-frame-fill.png differ diff --git a/h5/assets/recharge/reward-frame-mask-240.png b/h5/assets/recharge/reward-frame-mask-240.png new file mode 100644 index 0000000..dac15b7 --- /dev/null +++ b/h5/assets/recharge/reward-frame-mask-240.png @@ -0,0 +1,3 @@ + + + diff --git a/h5/assets/recharge/reward-icon.png b/h5/assets/recharge/reward-icon.png new file mode 100644 index 0000000..7096656 Binary files /dev/null and b/h5/assets/recharge/reward-icon.png differ diff --git a/h5/assets/recharge/section-badge.png b/h5/assets/recharge/section-badge.png new file mode 100644 index 0000000..94fe273 Binary files /dev/null and b/h5/assets/recharge/section-badge.png differ diff --git a/h5/hyapp/recharge-center/index.html b/h5/hyapp/recharge-center/index.html new file mode 100644 index 0000000..25aaf55 --- /dev/null +++ b/h5/hyapp/recharge-center/index.html @@ -0,0 +1,122 @@ + + + + + + Recharge Center + + + +
+ + + +
+
+
+
+
Recharge user
+
Search by user ID
+
+ +
+
+ + +
+ +
+ + + + + + +
+ + + + +
+ +
loading....
+
+ + +
+ + + diff --git a/h5/hyapp/recharge-center/original-vue-logic.md b/h5/hyapp/recharge-center/original-vue-logic.md new file mode 100644 index 0000000..a46cb6c --- /dev/null +++ b/h5/hyapp/recharge-center/original-vue-logic.md @@ -0,0 +1,163 @@ +# Original Vue Recharge Center Logic + +## Routes + +- `/recharge` + - Clears the persisted `targetUser` store on mount. + - Lets the user input a target account / user ID. + - Searches the target user before entering the payment page. + - Opens a help modal with recharge instructions. +- `/recharge-pay-way` + - Reads the target user from the persisted `targetUser` store. + - Loads payment application, target region/country, commodity packages, and payment channels. + - Places a recharge order and redirects to a third-party payment page. + +## State + +- `targetUser` + - Persisted Pinia store. + - `setTargetUser(profile)` after `/recharge` search succeeds. + - `clearTargetUser()` on `/recharge` mount and after Airwallex order redirect starts. +- `selectedCountry` + - Country store value used by `/recharge-pay-way`. + - First returned country is selected when the store is empty. +- `countryList` + - Country store list populated from `/order/web/pay/user-profile`. +- `applicationId` + - Fixed value in original Vue: `1963531459019739137`. +- `type` + - `FREIGHT_GOLD_SUPER` when `targetUser.isSuperFreightAgent`. + - `FREIGHT_GOLD` when `targetUser.isFreightAgent`. + - `GOLD` otherwise. + +## APIs + +### Search target user + +`GET /user/user-profile/open-search?account=` + +Usage: +- Called from `/recharge` search button. +- Success condition in Vue: `status && body.id`. +- Success action: save `body` to `targetUser`, then route to `/recharge-pay-way`. +- Failure action: toast `User info not found`. + +Reusable fields: +- `id` +- `account` +- `userNickname` +- `name` +- `userAvatar` +- `ownSpecialId.account` +- `isFreightAgent` +- `isSuperFreightAgent` + +### Get payment application + +`GET /order/web/pay/application/{applicationId}` + +Usage: +- Called by `/recharge-pay-way`. +- Original code reads `body.appCode`. + +Reusable fields: +- `id` +- `appName` +- `appCode` +- `androidLink` +- `iosLink` + +### Get target recharge profile and countries + +`GET /order/web/pay/user-profile` + +Query params: +- `sysOrigin=` +- `account=` +- `type=GOLD | FREIGHT_GOLD | FREIGHT_GOLD_SUPER` + +Usage: +- Called after payment application is loaded. +- Backend resolves user region and available pay countries. +- Original Vue sets `countryList`, selects the first country if none is selected, and stores `regionId`. + +Reusable fields: +- `userProfile.id` +- `userProfile.userAvatar` +- `userProfile.userNickname` +- `userProfile.account` +- `userProfile.sysOrigin` +- `countryList[].id` +- `countryList[].countryId` +- `countryList[].country.countryName` +- `countryList[].country.aliasName` +- `countryList[].country.nationalFlag` +- `regionId` + +### Get commodities and channels + +`GET /order/web/pay/commodity` + +Query params: +- `applicationId=1963531459019739137` +- `payCountryId=` +- `regionId=` +- `type=GOLD | FREIGHT_GOLD | FREIGHT_GOLD_SUPER` + +Usage: +- Called after country/profile data is ready. +- Original Vue renders all returned channels, and each channel shows the same commodity grid. + +Reusable fields: +- `application` +- `commodity[].id` +- `commodity[].content` +- `commodity[].awardContent` +- `commodity[].amountUsd` +- `commodity[].amount` +- `commodity[].currency` +- `channels[].channel.channelCode` +- `channels[].channel.channelName` +- `channels[].channel.channelIcon` +- `channels[].details.channelCode` +- `channels[].details.factoryChannel` +- `channels[].details.factoryMinLimit` +- `channels[].details.factoryMaxLimit` + +### Place recharge order + +`POST /order/web/pay/recharge` + +Body: + +```json +{ + "applicationId": "1963531459019739137", + "payCountryId": "", + "goodsId": "", + "userId": "", + "channelCode": "" +} +``` + +Usage: +- Called when the user taps a commodity under a payment channel. +- If `body.factoryCode === "AIRWALLEX"`, original Vue loads Airwallex Elements and calls `redirectToCheckout`. +- If `body.requestUrl` exists, original Vue redirects with `location.href = decodeURIComponent(requestUrl)`. + +Reusable fields: +- `orderId` +- `tradeNo` +- `factoryCode` +- `factoryChannelCode` +- `currency` +- `requestUrl` +- `expand.id` +- `expand.client_secret` + +## New H5 implementation notes + +- New page lives at `h5/hyapp/recharge-center`. +- It keeps the existing hyapp center architecture: static `index.html`, `style.css`, `script.js`. +- It uses URL `token` for Java API authorization and sends the same H5 request headers used by the existing hyapp center pages. +- It merges the original `/recharge` and `/recharge-pay-way` flows into one page, so it does not depend on Vue router or Pinia `targetUser`. diff --git a/h5/hyapp/recharge-center/script.js b/h5/hyapp/recharge-center/script.js new file mode 100644 index 0000000..9ca46a4 --- /dev/null +++ b/h5/hyapp/recharge-center/script.js @@ -0,0 +1,1057 @@ +(function () { + const API_BASE_URL = "https://jvapi.haiyihy.com"; + const RECHARGE_APPLICATION_ID = "1963531459019739137"; + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + + const messages = { + en: { + document_title: "Recharge Center", + page_label: "Recharge Center", + back: "Back", + title: "Recharge Center", + language_button_aria: "Change language", + help: "Help", + close: "Close", + search_title: "Recharge user", + search_subtitle: "Search by user ID", + search_placeholder: "Enter user ID", + search: "Search", + searching: "Searching...", + target_user: "Target user", + uid_prefix: "UID:", + change: "Change", + select_country: "Select a country", + payment_methods: "Payment methods", + select_package: "Select a package", + loading_data: "Loading...", + no_country: "No supported country", + no_packages: "No recharge packages", + user_not_found: "User info not found", + missing_token: "Missing token", + failed_to_load: "Failed to load data", + payment_not_available: "Payment not available", + creating_order: "Creating order...", + redirecting_payment: "Redirecting to payment...", + order_created: "Order created", + faq_title: "FAQ", + faq_question_1: "How to recharge?", + faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", + faq_question_2: "How to check recharge result?", + faq_answer_2: "After payment, return to the app and check the user's wallet balance." + }, + ar: { + document_title: "Recharge Center", + page_label: "Recharge Center", + back: "Back", + title: "Recharge Center", + language_button_aria: "Change language", + help: "Help", + close: "Close", + search_title: "Recharge user", + search_subtitle: "Search by user ID", + search_placeholder: "Enter user ID", + search: "Search", + searching: "Searching...", + target_user: "Target user", + uid_prefix: "UID:", + change: "Change", + select_country: "Select a country", + payment_methods: "Payment methods", + select_package: "Select a package", + loading_data: "Loading...", + no_country: "No supported country", + no_packages: "No recharge packages", + user_not_found: "User info not found", + missing_token: "Missing token", + failed_to_load: "Failed to load data", + payment_not_available: "Payment not available", + creating_order: "Creating order...", + redirecting_payment: "Redirecting to payment...", + order_created: "Order created", + faq_title: "FAQ", + faq_question_1: "How to recharge?", + faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", + faq_question_2: "How to check recharge result?", + faq_answer_2: "After payment, return to the app and check the user's wallet balance." + }, + tr: { + document_title: "Recharge Center", + page_label: "Recharge Center", + back: "Geri", + title: "Recharge Center", + language_button_aria: "Dili degistir", + help: "Help", + close: "Kapat", + search_title: "Recharge user", + search_subtitle: "Kullanici ID ile ara", + search_placeholder: "Kullanici ID gir", + search: "Ara", + searching: "Araniyor...", + target_user: "Target user", + uid_prefix: "UID:", + change: "Degistir", + select_country: "Ulke sec", + payment_methods: "Payment methods", + select_package: "Paket sec", + loading_data: "Yukleniyor...", + no_country: "Desteklenen ulke yok", + no_packages: "Recharge package yok", + user_not_found: "Kullanici bulunamadi", + missing_token: "Token eksik", + failed_to_load: "Veri yuklenemedi", + payment_not_available: "Payment not available", + creating_order: "Order olusturuluyor...", + redirecting_payment: "Payment sayfasina yonlendiriliyor...", + order_created: "Order created", + faq_title: "FAQ", + faq_question_1: "How to recharge?", + faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", + faq_question_2: "How to check recharge result?", + faq_answer_2: "After payment, return to the app and check the user's wallet balance." + }, + id: { + document_title: "Recharge Center", + page_label: "Recharge Center", + back: "Kembali", + title: "Recharge Center", + language_button_aria: "Ubah bahasa", + help: "Help", + close: "Tutup", + search_title: "Recharge user", + search_subtitle: "Cari dengan user ID", + search_placeholder: "Masukkan user ID", + search: "Cari", + searching: "Mencari...", + target_user: "Target user", + uid_prefix: "UID:", + change: "Ganti", + select_country: "Pilih negara", + payment_methods: "Payment methods", + select_package: "Pilih paket", + loading_data: "Memuat...", + no_country: "Negara tidak tersedia", + no_packages: "Paket recharge tidak tersedia", + user_not_found: "User tidak ditemukan", + missing_token: "Token tidak ditemukan", + failed_to_load: "Gagal memuat data", + payment_not_available: "Payment not available", + creating_order: "Membuat order...", + redirecting_payment: "Membuka payment...", + order_created: "Order created", + faq_title: "FAQ", + faq_question_1: "How to recharge?", + faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.", + faq_question_2: "How to check recharge result?", + faq_answer_2: "After payment, return to the app and check the user's wallet balance." + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = messages[currentLang] || messages.en; + + const state = { + query: params.get("account") || params.get("targetAccount") || params.get("target") || "", + searchLocked: false, + loadingConfig: false, + ordering: false, + activeOrderKey: "", + searchStatusText: "", + searchStatusType: "", + paymentStatusText: "", + paymentStatusType: "", + profile: null, + application: null, + appCode: "", + rechargeType: "GOLD", + payProfile: null, + countryList: [], + selectedCountryId: "", + regionId: "", + commodity: [], + channels: [] + }; + + function readURLParams() { + const result = new URLSearchParams(window.location.search); + const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : ""; + const hashParams = new URLSearchParams(hashQuery); + hashParams.forEach((value, key) => { + if (!result.has(key)) result.set(key, value); + }); + return result; + } + + function readRawParam(name) { + const queries = [window.location.search.replace(/^\?/, "")]; + if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]); + + for (const query of queries) { + for (const part of query.split("&")) { + if (!part) continue; + const equalIndex = part.indexOf("="); + const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part; + const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : ""; + let decodedKey = ""; + try { + decodedKey = decodeURIComponent(rawKey); + } catch (error) { + continue; + } + if (decodedKey !== name) continue; + try { + return decodeURIComponent(rawValue.replace(/\+/g, "%2B")); + } catch (error) { + return rawValue; + } + } + } + return ""; + } + + function normalizeLanguage(lang) { + const value = String(lang || "en").toLowerCase(); + if (value.startsWith("ar")) return "ar"; + if (value.startsWith("tr")) return "tr"; + if (value.startsWith("id")) return "id"; + return supportedLanguages.includes(value) ? value : "en"; + } + + function message(key, fallback = "") { + return currentMessages[key] || messages.en[key] || fallback || key; + } + + function renderMessages(dictionary) { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = dictionary[node.dataset.i18n] || messages.en[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = dictionary[node.dataset.i18nAria] || messages.en[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", value); + }); + + document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { + const value = dictionary[node.dataset.i18nPlaceholder] || messages.en[node.dataset.i18nPlaceholder]; + if (value) node.setAttribute("placeholder", value); + }); + } + + function apiBaseURL() { + const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); + if (override) return String(override).replace(/\/+$/, ""); + return API_BASE_URL.replace(/\/+$/, ""); + } + + function normalizeAuthorization(token) { + const value = String(token || "").trim(); + if (!value) return ""; + return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; + } + + function authHeadersFromToken(token) { + return { + Authorization: normalizeAuthorization(token), + "req-lang": currentLang, + "req-client": "H5", + "req-version": "V2", + "req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai", + "req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web", + "req-sys-origin": "origin=ATYOU;originChild=ATYOU" + }; + } + + async function requestJSON(path, options = {}) { + const token = readRawParam("token"); + if (!token) throw new Error(message("missing_token")); + + const headers = authHeadersFromToken(token); + const request = { + method: options.method || "GET", + cache: "no-store", + headers + }; + + if (options.body !== undefined) { + headers["Content-Type"] = "application/json"; + request.body = JSON.stringify(options.body); + } + + const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), request); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + error.response = data; + throw error; + } + + return data.body ?? data.data ?? null; + } + + function endpointOpenSearch(account) { + return `/user/user-profile/open-search?account=${encodeURIComponent(account)}`; + } + + function endpointApplication() { + return `/order/web/pay/application/${encodeURIComponent(RECHARGE_APPLICATION_ID)}`; + } + + function endpointPayProfile(appCode, account, type) { + const query = new URLSearchParams({ + sysOrigin: appCode, + account, + type + }); + return `/order/web/pay/user-profile?${query.toString()}`; + } + + function endpointCommodity(countryId, regionId, type) { + const query = new URLSearchParams({ + applicationId: RECHARGE_APPLICATION_ID, + payCountryId: countryId, + regionId: regionId || "", + type + }); + return `/order/web/pay/commodity?${query.toString()}`; + } + + function setLoading(isLoading) { + const root = document.querySelector(".recharge-center"); + const mask = document.querySelector("#loadingMask"); + if (root) root.dataset.loading = isLoading ? "true" : "false"; + if (mask) mask.hidden = !isLoading; + } + + function updateModalLock() { + const hasOpen = Array.from(document.querySelectorAll(".help-modal")).some((modal) => !modal.hidden); + document.body.classList.toggle("modal-open", hasOpen); + } + + function setModalOpen(selector, open) { + const modal = document.querySelector(selector); + if (!modal) return; + modal.hidden = !open; + updateModalLock(); + } + + function showToast(text) { + const toast = document.querySelector("#toast"); + if (!toast) return; + toast.textContent = text || ""; + toast.hidden = !text; + window.clearTimeout(showToast.timer); + showToast.timer = window.setTimeout(() => { + toast.hidden = true; + }, 2500); + } + + function replaceChildren(node, children) { + while (node.firstChild) node.removeChild(node.firstChild); + children.forEach((child) => node.appendChild(child)); + } + + function formatMoney(value) { + const number = Number(value); + if (!Number.isFinite(number)) return "0.00"; + return number.toFixed(2); + } + + function pickSpecialAccount(profile) { + return profile?.ownSpecialId?.account || profile?.specialId?.account || profile?.specialAccount || profile?.shortAccount || profile?.shortId || ""; + } + + function pickProfileName(profile) { + return profile?.userNickname || profile?.name || "-"; + } + + function pickDisplayAccount(profile) { + return pickSpecialAccount(profile) || profile?.account || profile?.id || "-"; + } + + function pickApiAccount(profile) { + return profile?.account || state.query || pickSpecialAccount(profile) || ""; + } + + function pickProfileId(profile) { + return profile?.id || profile?.userId || ""; + } + + function commodityType(profile = state.profile) { + if (profile?.isSuperFreightAgent) return "FREIGHT_GOLD_SUPER"; + if (profile?.isFreightAgent) return "FREIGHT_GOLD"; + return "GOLD"; + } + + function setSearchStatus(text = "", type = "") { + state.searchStatusText = text; + state.searchStatusType = type; + const status = document.querySelector("#searchStatus"); + if (!status) return; + status.textContent = text; + status.hidden = !text; + status.classList.toggle("is-error", type === "error"); + } + + function setPaymentStatus(text = "", type = "") { + state.paymentStatusText = text; + state.paymentStatusType = type; + const status = document.querySelector("#paymentStatus"); + if (!status) return; + status.textContent = text; + status.hidden = !text; + status.classList.toggle("is-error", type === "error"); + } + + function renderSearch() { + const input = document.querySelector("#accountInput"); + const button = document.querySelector("#searchButton"); + if (input && document.activeElement !== input) input.value = state.query; + if (button) { + const hasQuery = Boolean(String(state.query || "").trim()); + button.disabled = state.searchLocked || !hasQuery; + button.textContent = state.searchLocked ? message("searching") : message("search"); + } + setSearchStatus(state.searchStatusText, state.searchStatusType); + } + + function renderAvatar(profile) { + const image = document.querySelector("#targetAvatar"); + const fallback = document.querySelector("#targetAvatarFallback"); + if (!image || !fallback) return; + + const name = pickProfileName(profile); + const avatar = profile?.userAvatar || profile?.avatar || ""; + fallback.textContent = String(name || "U").slice(0, 1).toUpperCase() || "U"; + + if (!avatar) { + image.hidden = true; + image.removeAttribute("src"); + fallback.hidden = false; + return; + } + + image.alt = name; + fallback.hidden = true; + image.hidden = false; + image.onerror = () => { + image.hidden = true; + image.removeAttribute("src"); + fallback.hidden = false; + }; + image.src = avatar; + } + + function renderTarget() { + const card = document.querySelector("#targetCard"); + if (card) card.hidden = !state.profile; + if (!state.profile) return; + + const nameNode = document.querySelector("#targetName"); + const uidNode = document.querySelector("#targetUid"); + if (nameNode) nameNode.textContent = pickProfileName(state.profile); + if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${pickDisplayAccount(state.profile)}`.trim(); + renderAvatar(state.profile); + } + + function countryName(item) { + return item?.countryName || item?.country?.aliasName || item?.country?.countryName || item?.name || "-"; + } + + function countryFlag(item) { + return item?.nationalFlag || item?.country?.nationalFlag || item?.flag || ""; + } + + function selectedCountry() { + return state.countryList.find((item) => String(item?.id) === String(state.selectedCountryId)) || state.countryList[0] || null; + } + + function createCountryChip(item) { + const button = document.createElement("button"); + button.className = "country-chip"; + button.type = "button"; + const active = String(item?.id) === String(state.selectedCountryId); + button.classList.toggle("is-active", active); + button.disabled = state.loadingConfig || state.ordering; + button.addEventListener("click", () => selectCountry(item?.id)); + + const flag = document.createElement("span"); + flag.className = "country-flag"; + const flagUrl = countryFlag(item); + if (flagUrl) { + const image = document.createElement("img"); + image.src = flagUrl; + image.alt = ""; + image.addEventListener("error", () => { + flag.textContent = String(countryName(item)).slice(0, 2).toUpperCase(); + }); + flag.appendChild(image); + } else { + flag.textContent = String(countryName(item)).slice(0, 2).toUpperCase(); + } + + const name = document.createElement("span"); + name.className = "country-name"; + name.textContent = countryName(item); + button.append(flag, name); + return button; + } + + function renderCountries() { + const card = document.querySelector("#countryCard"); + const list = document.querySelector("#countryList"); + const subtitle = document.querySelector("#countrySubtitle"); + if (card) card.hidden = !state.profile; + if (!list || !state.profile) return; + + const country = selectedCountry(); + if (subtitle) subtitle.textContent = country ? countryName(country) : message("no_country"); + + if (state.loadingConfig && !state.countryList.length) { + const loading = document.createElement("div"); + loading.className = "empty-state"; + loading.textContent = message("loading_data"); + replaceChildren(list, [loading]); + return; + } + + if (!state.countryList.length) { + const empty = document.createElement("div"); + empty.className = "empty-state"; + empty.textContent = message("no_country"); + replaceChildren(list, [empty]); + return; + } + + replaceChildren(list, state.countryList.map(createCountryChip)); + } + + function channelCode(item) { + return item?.channel?.channelCode || item?.details?.channelCode || item?.channelCode || ""; + } + + function channelName(item) { + return item?.channel?.channelName || item?.channelName || channelCode(item) || "-"; + } + + function channelIcon(item) { + return item?.channel?.channelIcon || item?.channelIcon || ""; + } + + function createChannelIcon(item) { + const icon = document.createElement("div"); + icon.className = "channel-icon"; + const url = channelIcon(item); + if (!url) { + icon.textContent = String(channelName(item)).slice(0, 1).toUpperCase(); + return icon; + } + const image = document.createElement("img"); + image.src = url; + image.alt = ""; + image.addEventListener("error", () => { + icon.textContent = String(channelName(item)).slice(0, 1).toUpperCase(); + }); + icon.appendChild(image); + return icon; + } + + function packageKey(channel, goods) { + return `${channelCode(channel)}:${goods?.id || ""}`; + } + + function createPackageButton(channel, goods) { + const button = document.createElement("button"); + button.className = "package-button"; + button.type = "button"; + const key = packageKey(channel, goods); + button.disabled = state.ordering || state.loadingConfig; + button.addEventListener("click", () => placeOrder(channel, goods)); + + const coin = document.createElement("span"); + coin.className = "coin-mark"; + coin.textContent = "$"; + const content = document.createElement("span"); + content.className = "package-content"; + content.textContent = goods?.content || goods?.awardContent || "-"; + const price = document.createElement("span"); + price.className = "package-price"; + price.textContent = state.ordering && state.activeOrderKey === key + ? message("creating_order") + : `$${formatMoney(goods?.amountUsd)}`; + + button.append(coin, content, price); + return button; + } + + function createChannelCard(item) { + const card = document.createElement("article"); + card.className = "channel-card"; + + const head = document.createElement("div"); + head.className = "channel-head"; + const name = document.createElement("div"); + name.className = "channel-name"; + name.textContent = channelName(item); + head.append(createChannelIcon(item), name); + + const grid = document.createElement("div"); + grid.className = "package-grid"; + state.commodity.forEach((goods) => grid.appendChild(createPackageButton(item, goods))); + + card.append(head, grid); + return card; + } + + function renderPayment() { + const card = document.querySelector("#paymentCard"); + const list = document.querySelector("#channelList"); + if (card) card.hidden = !state.profile; + if (!list || !state.profile) return; + + setPaymentStatus(state.paymentStatusText, state.paymentStatusType); + + if (state.loadingConfig) { + const loading = document.createElement("div"); + loading.className = "empty-state"; + loading.textContent = message("loading_data"); + replaceChildren(list, [loading]); + return; + } + + if (!state.channels.length || !state.commodity.length) { + const empty = document.createElement("div"); + empty.className = "empty-state"; + empty.textContent = message("no_packages"); + replaceChildren(list, [empty]); + return; + } + + replaceChildren(list, state.channels.map(createChannelCard)); + } + + function renderAll() { + renderSearch(); + renderTarget(); + renderCountries(); + renderPayment(); + } + + async function fetchApplication() { + state.application = await requestJSON(endpointApplication()) || {}; + state.appCode = state.application.appCode || ""; + return state.application; + } + + async function fetchRechargeProfile() { + if (!state.application || !state.appCode) await fetchApplication(); + const account = pickApiAccount(state.profile); + state.rechargeType = commodityType(state.profile); + const data = await requestJSON(endpointPayProfile(state.appCode, account, state.rechargeType)); + state.payProfile = data?.userProfile || null; + state.countryList = Array.isArray(data?.countryList) ? data.countryList : []; + state.regionId = data?.regionId || ""; + if (state.payProfile) { + state.profile = { + ...state.profile, + ...state.payProfile, + isFreightAgent: state.profile?.isFreightAgent, + isSuperFreightAgent: state.profile?.isSuperFreightAgent + }; + } + if (!state.countryList.some((item) => String(item?.id) === String(state.selectedCountryId))) { + state.selectedCountryId = state.countryList[0]?.id ? String(state.countryList[0].id) : ""; + } + } + + async function fetchCommodities() { + const country = selectedCountry(); + if (!country?.id) { + state.commodity = []; + state.channels = []; + return; + } + const data = await requestJSON(endpointCommodity(String(country.id), state.regionId, state.rechargeType)); + state.commodity = Array.isArray(data?.commodity) ? data.commodity : []; + state.channels = Array.isArray(data?.channels) ? data.channels : []; + if (data?.application) state.application = data.application; + } + + async function loadRechargeOptions() { + state.loadingConfig = true; + state.paymentStatusText = message("loading_data"); + state.paymentStatusType = ""; + state.countryList = []; + state.selectedCountryId = ""; + state.commodity = []; + state.channels = []; + renderAll(); + + try { + await fetchRechargeProfile(); + await fetchCommodities(); + setPaymentStatus(""); + } catch (error) { + console.error("Failed to load recharge options:", error); + state.paymentStatusText = error.response?.errorMsg || error.message || message("failed_to_load"); + state.paymentStatusType = "error"; + showToast(state.paymentStatusText); + } finally { + state.loadingConfig = false; + renderAll(); + } + } + + async function searchTarget(event) { + if (event) event.preventDefault(); + if (state.searchLocked) return; + + const input = document.querySelector("#accountInput"); + const query = String(input?.value || state.query || "").trim(); + state.query = query; + if (!query) { + setSearchStatus(""); + renderAll(); + return; + } + + state.searchLocked = true; + state.profile = null; + state.payProfile = null; + state.countryList = []; + state.selectedCountryId = ""; + state.commodity = []; + state.channels = []; + setSearchStatus(message("searching")); + setPaymentStatus(""); + renderAll(); + + try { + const result = await requestJSON(endpointOpenSearch(query)); + const profile = Array.isArray(result) ? result[0] : result; + if (!profile || !pickProfileId(profile)) throw new Error(message("user_not_found")); + state.profile = profile; + setSearchStatus(""); + renderAll(); + await loadRechargeOptions(); + } catch (error) { + console.error("Recharge target search failed:", error); + const text = error.response?.errorMsg || error.message || message("user_not_found"); + setSearchStatus(text, "error"); + showToast(text); + } finally { + state.searchLocked = false; + renderAll(); + } + } + + async function selectCountry(id) { + if (!id || String(id) === String(state.selectedCountryId) || state.loadingConfig || state.ordering) return; + + state.selectedCountryId = String(id); + state.loadingConfig = true; + state.commodity = []; + state.channels = []; + state.paymentStatusText = message("loading_data"); + state.paymentStatusType = ""; + renderAll(); + + try { + await fetchCommodities(); + setPaymentStatus(""); + } catch (error) { + console.error("Failed to switch recharge country:", error); + state.paymentStatusText = error.response?.errorMsg || error.message || message("failed_to_load"); + state.paymentStatusType = "error"; + showToast(state.paymentStatusText); + } finally { + state.loadingConfig = false; + renderAll(); + } + } + + function payEnvironment() { + const value = String(window.AIRWALLEX_ENV || params.get("payEnv") || window.ENV || "").toLowerCase(); + if (value === "production" || value === "prod") return "prod"; + if (value === "staging") return "staging"; + if (value === "dev") return "dev"; + if (value === "local") return "local"; + return "demo"; + } + + function airwallexHost(env) { + const hosts = { + prod: "checkout.airwallex.com", + demo: "checkout-demo.airwallex.com", + staging: "checkout-staging.airwallex.com", + dev: "checkout-dev.airwallex.com", + local: "localhost:3000" + }; + return `https://${hosts[env] || hosts.prod}`; + } + + function loadAirwallex(env) { + if (window.Airwallex) { + window.Airwallex.init({ env }); + return Promise.resolve(window.Airwallex); + } + + const src = `${airwallexHost(env)}/assets/elements.bundle.min.js?version=1.160.0`; + const existing = document.querySelector(`[data-airwallex-script="${env}"]`); + const script = existing || document.createElement("script"); + if (!existing) { + script.src = src; + script.crossOrigin = "anonymous"; + script.dataset.airwallexScript = env; + document.head.appendChild(script); + } + + return new Promise((resolve, reject) => { + script.addEventListener("load", () => { + if (!window.Airwallex) { + reject(new Error("Failed to load Airwallex")); + return; + } + window.Airwallex.init({ env }); + resolve(window.Airwallex); + }, { once: true }); + script.addEventListener("error", () => reject(new Error("Failed to load Airwallex")), { once: true }); + }); + } + + function parseExpand(value) { + if (!value) return {}; + if (typeof value === "object") return value; + try { + return JSON.parse(value); + } catch (error) { + return {}; + } + } + + function paymentResultURL(status) { + const type = params.get("type") || ""; + const query = new URLSearchParams({ + redirect: "recharge", + appType: type, + appId: RECHARGE_APPLICATION_ID, + appStatus: status + }); + return `${window.location.origin}/#/pay-result?${query.toString()}`; + } + + async function redirectAirwallex(result) { + const env = payEnvironment(); + const expand = parseExpand(result.expand); + if (!expand.id || !expand.client_secret) throw new Error(message("payment_not_available")); + await loadAirwallex(env); + window.Airwallex.redirectToCheckout({ + env, + mode: "payment", + currency: result.currency, + intent_id: expand.id, + client_secret: expand.client_secret, + methods: result.factoryChannelCode ? [result.factoryChannelCode] : undefined, + recurringOptions: { + card: { + next_triggered_by: "customer", + merchant_trigger_reason: "unscheduled", + currency: result.currency + } + }, + logoUrl: window.sysOriginLog || "", + successUrl: paymentResultURL("0"), + failUrl: paymentResultURL("1"), + theme: { + fonts: [ + { + src: "https://checkout.airwallex.com/fonts/CircularXXWeb/CircularXXWeb-Regular.woff2", + family: "AxLLCircular", + weight: 400 + } + ] + } + }); + } + + async function handlePaymentResponse(result) { + if (!result) throw new Error(message("payment_not_available")); + + if (result.factoryCode === "AIRWALLEX") { + setPaymentStatus(message("redirecting_payment")); + await redirectAirwallex(result); + return; + } + + if (result.requestUrl) { + setPaymentStatus(message("redirecting_payment")); + window.location.href = decodeURIComponent(result.requestUrl); + return; + } + + setPaymentStatus(message("order_created")); + showToast(message("order_created")); + } + + async function placeOrder(channel, goods) { + if (state.ordering || state.loadingConfig) return; + const country = selectedCountry(); + const code = channelCode(channel); + const userId = pickProfileId(state.payProfile || state.profile); + + if (!country?.id || !goods?.id || !userId || !code) { + showToast(message("payment_not_available")); + return; + } + + state.ordering = true; + state.activeOrderKey = packageKey(channel, goods); + setPaymentStatus(message("creating_order")); + renderAll(); + + try { + const result = await requestJSON("/order/web/pay/recharge", { + method: "POST", + body: { + applicationId: RECHARGE_APPLICATION_ID, + payCountryId: String(country.id), + goodsId: String(goods.id), + userId: String(userId), + channelCode: code + } + }); + await handlePaymentResponse(result); + } catch (error) { + console.error("Recharge order failed:", error); + const text = error.response?.errorMsg || error.message || message("payment_not_available"); + setPaymentStatus(text, "error"); + showToast(text); + } finally { + state.ordering = false; + state.activeOrderKey = ""; + renderAll(); + } + } + + function changeTarget() { + state.profile = null; + state.payProfile = null; + state.countryList = []; + state.selectedCountryId = ""; + state.regionId = ""; + state.commodity = []; + state.channels = []; + state.paymentStatusText = ""; + state.paymentStatusType = ""; + renderAll(); + window.setTimeout(() => document.querySelector("#accountInput")?.focus(), 60); + } + + function closePage() { + try { + if (window.app && typeof window.app.closePage === "function") { + window.app.closePage(); + return; + } + + if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") { + window.FlutterPageControl.postMessage("close_page"); + return; + } + + if (window.history.length > 1) window.history.back(); + } catch (error) { + console.error("close recharge center page failed:", error); + if (window.history.length > 1) window.history.back(); + } + } + + function setLanguageMenuOpen(open) { + const button = document.querySelector(".language-button"); + const menu = document.querySelector(".language-menu"); + if (!button || !menu) return; + menu.hidden = !open; + button.setAttribute("aria-expanded", String(open)); + } + + function renderLanguageControls() { + const button = document.querySelector(".language-button"); + if (button) { + button.textContent = languageLabels[currentLang]; + button.setAttribute("aria-label", message("language_button_aria", "Change language")); + } + document.querySelectorAll(".language-menu [data-lang]").forEach((node) => { + const active = node.dataset.lang === currentLang; + node.classList.toggle("is-active", active); + node.setAttribute("aria-pressed", String(active)); + }); + } + + function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + currentMessages = messages[currentLang] || messages.en; + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = message("document_title", "Recharge Center"); + renderMessages(currentMessages); + renderLanguageControls(); + renderAll(); + } + + function bindEvents() { + document.querySelector(".back-button")?.addEventListener("click", closePage); + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const menu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(menu?.hidden)); + }); + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + button.addEventListener("click", (event) => { + event.stopPropagation(); + setLanguageMenuOpen(false); + applyLanguage(button.dataset.lang); + }); + }); + document.addEventListener("click", () => setLanguageMenuOpen(false)); + document.querySelector("#accountInput")?.addEventListener("input", (event) => { + state.query = String(event.target.value || ""); + if (!state.query.trim()) setSearchStatus(""); + renderSearch(); + }); + document.querySelector("#searchForm")?.addEventListener("submit", searchTarget); + document.querySelector("[data-change-target]")?.addEventListener("click", changeTarget); + document.querySelector("[data-help-open]")?.addEventListener("click", () => setModalOpen("#helpModal", true)); + document.querySelectorAll("[data-help-close]").forEach((node) => { + node.addEventListener("click", () => setModalOpen("#helpModal", false)); + }); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") setModalOpen("#helpModal", false); + }); + } + + async function initializePage() { + setLoading(true); + bindEvents(); + applyLanguage(currentLang); + renderAll(); + setLoading(false); + + if (!readRawParam("token")) { + setSearchStatus(message("missing_token"), "error"); + renderAll(); + return; + } + + if (state.query) { + await searchTarget(); + } + } + + initializePage(); +})(); diff --git a/h5/hyapp/recharge-center/style.css b/h5/hyapp/recharge-center/style.css new file mode 100644 index 0000000..9e0880a --- /dev/null +++ b/h5/hyapp/recharge-center/style.css @@ -0,0 +1,859 @@ +:root { + --page-bg: #f6f7f7; + --card-bg: rgba(255, 255, 255, 0.96); + --text: #25282e; + --muted: #8f9196; + --line: #eef0f2; + --mint: #43e7d8; + --green: #25d878; + --danger: #e85b5b; + --shadow: 0 8px 22px rgba(36, 46, 60, 0.055); +} + +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + min-height: 100%; + margin: 0; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body { + display: flex; + justify-content: center; + background: #111; + color: var(--text); + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +button, +input { + font: inherit; +} + +button { + border: 0; + padding: 0; + background: transparent; + color: inherit; +} + +[hidden] { + display: none !important; +} + +.recharge-center { + position: relative; + width: 100%; + max-width: 430px; + min-height: 100vh; + overflow: hidden; + background: var(--page-bg); +} + +.recharge-center[data-loading="true"] .title-bar, +.recharge-center[data-loading="true"] .content, +.recharge-center[data-loading="true"] .home-indicator { + visibility: hidden; +} + +.hero-bg { + position: absolute; + inset: 0 0 auto; + height: 215px; + background: + radial-gradient(circle at 72% 4%, rgba(209, 255, 248, 0.98) 0, rgba(209, 255, 248, 0.88) 23%, rgba(209, 255, 248, 0) 48%), + radial-gradient(circle at 14% 20%, rgba(185, 250, 244, 0.78) 0, rgba(185, 250, 244, 0) 35%), + linear-gradient(151deg, #ccf7ef 0%, #ebfffa 58%, rgba(242, 255, 252, 0.7) 100%); + clip-path: polygon(0 0, 100% 0, 100% 76%, 70% 47%, 40% 58%, 0 78%); +} + +.title-bar, +.content, +.home-indicator { + position: relative; + z-index: 1; +} + +.title-bar { + display: grid; + grid-template-columns: 44px minmax(0, 1fr) 56px; + align-items: center; + z-index: 12; + min-height: 56px; + padding: 8px 14px 6px; +} + +.back-button, +.language-button, +.icon-button { + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: #2c3036; +} + +.back-button { + width: 38px; + height: 38px; +} + +.back-button svg, +.icon-button svg { + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; +} + +.back-button svg { + width: 30px; + height: 30px; + stroke-width: 2.4; +} + +.title-bar h1 { + min-width: 0; + margin: 0; + overflow: hidden; + color: #24282e; + font-size: 19px; + font-weight: 900; + line-height: 1.2; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.language-switcher { + position: relative; + display: flex; + justify-content: flex-end; +} + +.language-button { + width: auto; + min-width: 42px; + height: 32px; + padding: 0 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.55); + font-size: 12px; + font-weight: 900; +} + +.language-menu { + position: absolute; + top: 38px; + right: 0; + z-index: 30; + display: grid; + gap: 4px; + min-width: 74px; + padding: 8px; + border: 1px solid rgba(34, 196, 180, 0.18); + border-radius: 8px; + background: #fff; + box-shadow: 0 12px 24px rgba(22, 31, 42, 0.12); +} + +.language-menu button { + min-height: 30px; + border-radius: 6px; + font-size: 12px; + font-weight: 850; +} + +.language-menu button.is-active { + background: #e7fbf8; + color: #0aa397; +} + +.content { + display: grid; + gap: 12px; + padding: 8px 14px 32px; +} + +.card { + border-radius: 8px; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.search-card, +.country-card, +.payment-card { + display: grid; + gap: 14px; + padding: 16px; +} + +.section-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.section-title { + color: #24282e; + font-size: 16px; + font-weight: 900; + line-height: 1.2; +} + +.section-subtitle { + margin-top: 6px; + color: #93959a; + font-size: 12px; + font-weight: 800; + line-height: 1.2; +} + +.icon-button { + width: 34px; + height: 34px; + flex: 0 0 34px; + background: #e7fbf8; + color: #0aa397; +} + +.icon-button svg { + width: 21px; + height: 21px; + stroke-width: 2.2; +} + +.search-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; +} + +.search-form input { + width: 100%; + min-width: 0; + height: 44px; + border: 1px solid #e4eeee; + border-radius: 8px; + outline: 0; + padding: 0 12px; + background: #fbfdfd; + color: #24282e; + font-size: 14px; + font-weight: 800; +} + +.search-form input:focus { + border-color: rgba(21, 189, 169, 0.7); + box-shadow: 0 0 0 3px rgba(21, 189, 169, 0.12); +} + +.search-form button, +.package-button, +.mini-action { + min-height: 44px; + border-radius: 8px; + background: linear-gradient(135deg, #30dfb4, #15bda9); + color: #fff; + font-size: 14px; + font-weight: 950; + box-shadow: 0 8px 18px rgba(21, 189, 169, 0.24); +} + +.search-form button { + min-width: 88px; + padding: 0 14px; +} + +.search-form button:disabled, +.package-button:disabled, +.mini-action:disabled { + opacity: 0.55; +} + +.form-status, +.payment-status { + padding: 10px 12px; + border-radius: 8px; + background: #f1fbf9; + color: #0b958a; + font-size: 12px; + font-weight: 850; + line-height: 1.3; +} + +.form-status.is-error, +.payment-status.is-error { + background: #fff3f3; + color: var(--danger); +} + +.profile-card { + display: flex; + align-items: center; + min-height: 104px; + padding: 16px; +} + +.avatar-shell { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + width: 70px; + height: 70px; + flex: 0 0 70px; + border-radius: 50%; + background: linear-gradient(145deg, #d9fff5, #43e7d8); +} + +.avatar-image { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-fallback { + color: #0b5f56; + font-size: 26px; + font-weight: 950; +} + +.profile-copy { + min-width: 0; + flex: 1; + margin-left: 16px; +} + +.role-pill { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0 9px; + border-radius: 999px; + background: #e7fbf8; + color: #0b958a; + font-size: 11px; + font-weight: 900; +} + +.name { + margin-top: 7px; + overflow: hidden; + color: #25282e; + font-size: 20px; + font-weight: 900; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.meta { + margin-top: 6px; + overflow: hidden; + color: var(--muted); + font-size: 13px; + font-weight: 750; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mini-action { + min-width: 78px; + min-height: 36px; + margin-left: 10px; + padding: 0 12px; + font-size: 12px; +} + +.country-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); + gap: 10px; +} + +.country-chip { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + min-height: 46px; + padding: 8px 10px; + border: 1px solid #e4eeee; + border-radius: 8px; + background: #fbfdfd; +} + +.country-chip.is-active { + border-color: rgba(21, 189, 169, 0.6); + background: #ecfffb; + box-shadow: inset 0 0 0 1px rgba(21, 189, 169, 0.14); +} + +.country-flag { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + width: 28px; + height: 28px; + flex: 0 0 28px; + border-radius: 50%; + background: #e7fbf8; + color: #0b958a; + font-size: 11px; + font-weight: 950; +} + +.country-flag img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.country-name { + min-width: 0; + overflow: hidden; + color: #464951; + font-size: 13px; + font-weight: 900; + line-height: 1.2; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.channel-list { + display: grid; + gap: 12px; +} + +.channel-card { + display: grid; + gap: 12px; + padding: 13px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.channel-head { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.channel-icon { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + width: 38px; + height: 38px; + flex: 0 0 38px; + border-radius: 8px; + background: #e9fbf4; + color: #1f9d66; + font-size: 14px; + font-weight: 950; +} + +.channel-icon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.channel-name { + min-width: 0; + overflow: hidden; + color: #25282e; + font-size: 15px; + font-weight: 950; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.package-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; +} + +.package-button { + display: grid; + justify-items: center; + gap: 5px; + min-width: 0; + min-height: 92px; + padding: 10px 6px; + background: #fff; + color: #25282e; + border: 1px solid #eef0f2; + box-shadow: none; +} + +.coin-mark { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(145deg, #ffe694, #ffc93f); + color: #8b5d00; + font-weight: 950; +} + +.package-content, +.package-price { + max-width: 100%; + overflow-wrap: anywhere; + text-align: center; + line-height: 1.15; +} + +.package-content { + color: #4a4f58; + font-size: 12px; + font-weight: 850; +} + +.package-price { + color: #181d24; + font-size: 14px; + font-weight: 950; +} + +.empty-state { + padding: 14px; + border-radius: 8px; + background: #f7fbfa; + color: #8b8d92; + font-size: 13px; + font-weight: 850; + text-align: center; +} + +.help-modal { + position: fixed; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.modal-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.42); +} + +.modal-dialog { + position: relative; + z-index: 1; + display: grid; + gap: 12px; + width: min(100%, 360px); + max-height: 80vh; + overflow: hidden; + border-radius: 12px; + background: #fff; + box-shadow: 0 20px 46px rgba(15, 23, 42, 0.22); +} + +.modal-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 16px 0; +} + +.modal-head h2 { + margin: 0; + color: #24282e; + font-size: 18px; + font-weight: 950; + line-height: 1.2; +} + +.modal-close { + width: 30px; + height: 30px; + border-radius: 50%; + background: #f2f6f6; + color: #4b5563; + font-size: 22px; + line-height: 1; +} + +.help-content { + display: grid; + gap: 8px; + overflow-y: auto; + padding: 0 16px 18px; + -webkit-overflow-scrolling: touch; +} + +.help-content h3, +.help-content p { + margin: 0; +} + +.help-content h3 { + color: #24282e; + font-size: 15px; + font-weight: 950; +} + +.help-content p { + color: #5d626b; + font-size: 13px; + font-weight: 750; + line-height: 1.4; +} + +.home-indicator { + width: 134px; + height: 5px; + margin: 10px auto 8px; + border-radius: 999px; + background: #111; +} + +.loading-mask { + position: fixed; + inset: 0; + z-index: 60; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + background: rgba(246, 247, 247, 0.92); +} + +.runner-stick { + position: relative; + width: 74px; + height: 58px; + color: #16b86d; +} + +.runner-stick span { + position: absolute; + display: block; + background: currentColor; +} + +.runner-head { + top: 0; + left: 28px; + width: 18px; + height: 18px; + border-radius: 50%; + animation: runner-bob 0.5s linear infinite; +} + +.runner-body { + top: 18px; + left: 36px; + width: 5px; + height: 23px; + border-radius: 999px; + transform-origin: top center; + animation: runner-body 0.5s linear infinite; +} + +.runner-arm, +.runner-leg { + left: 36px; + width: 5px; + border-radius: 999px; + transform-origin: top center; +} + +.runner-arm { + top: 21px; + height: 22px; +} + +.runner-leg { + top: 39px; + height: 24px; +} + +.runner-arm-front { + animation: runner-arm-front 0.5s linear infinite; +} + +.runner-arm-back { + animation: runner-arm-back 0.5s linear infinite; +} + +.runner-leg-front { + animation: runner-leg-front 0.5s linear infinite; +} + +.runner-leg-back { + animation: runner-leg-back 0.5s linear infinite; +} + +.runner-ground { + left: 7px; + bottom: 0; + width: 58px; + height: 4px; + border-radius: 999px; + opacity: 0.35; + animation: runner-ground 0.5s linear infinite; +} + +.loading-copy { + color: #0b958a; + font-size: 13px; + font-weight: 950; + letter-spacing: 0; +} + +.toast { + position: fixed; + left: 50%; + bottom: calc(28px + env(safe-area-inset-bottom)); + z-index: 70; + max-width: min(330px, calc(100vw - 48px)); + transform: translateX(-50%); + padding: 10px 14px; + border-radius: 999px; + background: rgba(20, 27, 34, 0.92); + color: #fff; + font-size: 13px; + font-weight: 850; + line-height: 1.25; + text-align: center; +} + +body.modal-open { + overflow: hidden; +} + +[dir="rtl"] .language-menu { + right: auto; + left: 0; +} + +[dir="rtl"] .profile-copy { + margin-right: 16px; + margin-left: 0; +} + +[dir="rtl"] .mini-action { + margin-right: 10px; + margin-left: 0; +} + +[dir="rtl"] .country-name { + text-align: right; +} + +@keyframes runner-bob { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(4px); + } +} + +@keyframes runner-body { + 0%, + 100% { + transform: rotate(-8deg); + } + 50% { + transform: rotate(8deg); + } +} + +@keyframes runner-arm-front { + 0%, + 100% { + transform: rotate(60deg); + } + 50% { + transform: rotate(-55deg); + } +} + +@keyframes runner-arm-back { + 0%, + 100% { + transform: rotate(-55deg); + } + 50% { + transform: rotate(60deg); + } +} + +@keyframes runner-leg-front { + 0%, + 100% { + transform: rotate(-50deg); + } + 50% { + transform: rotate(48deg); + } +} + +@keyframes runner-leg-back { + 0%, + 100% { + transform: rotate(48deg); + } + 50% { + transform: rotate(-50deg); + } +} + +@keyframes runner-ground { + 0% { + transform: translateX(7px) scaleX(0.75); + } + 100% { + transform: translateX(-7px) scaleX(1); + } +} + +@media (max-width: 360px) { + .title-bar { + grid-template-columns: 40px minmax(0, 1fr) 50px; + padding-right: 10px; + padding-left: 10px; + } + + .content { + padding-right: 10px; + padding-left: 10px; + } + + .package-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +}