diff --git a/h5/activity/room-turnover-reward/index.html b/h5/activity/room-turnover-reward/index.html new file mode 100644 index 0000000..de47a22 --- /dev/null +++ b/h5/activity/room-turnover-reward/index.html @@ -0,0 +1,744 @@ + + + + + + + Room Reward Event + + + + + + +
+
+
+
+ +
+
+ + + + +
+ 1000 + +
+
+ +
+
+
Room turnover
+
Receive a reward
+
+
+
+
+ + +
+ + + + + diff --git a/h5/app-invite/app.js b/h5/app-invite/app.js index 92a7b6d..485b15b 100644 --- a/h5/app-invite/app.js +++ b/h5/app-invite/app.js @@ -6,6 +6,7 @@ const LOCAL_FILE_PROXY_API_BASE = "http://127.0.0.1:8800/api/"; const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/"; const ONLINE_INVITE_API_PREFIX = "/go"; const DEFAULT_AVATAR = "./assets/avatar-sample.png"; +const INVITEE_DISPLAY_GIFT_REWARD = 1000; const DEFAULT_RESET_PERIOD_MS = 30 * 86400 * 1000; const DEFAULT_REQUEST_HEADERS = { "Req-Sys-Origin": "origin=LIKEI;originChild=LIKEI", @@ -32,7 +33,7 @@ const DEFAULTS = { downloadUrl: "", inviterBindReward: 0, rechargeReward: 0, - giftReward: 0, + giftReward: INVITEE_DISPLAY_GIFT_REWARD, }; const FALLBACK_TASKS = { @@ -58,7 +59,7 @@ const state = { downloadUrl: query.get("downloadUrl") || DEFAULTS.downloadUrl, inviterBindReward: query.get("inviterReward") || DEFAULTS.inviterBindReward, rechargeReward: query.get("rechargeReward") || DEFAULTS.rechargeReward, - giftReward: query.get("giftReward") || DEFAULTS.giftReward, + giftReward: INVITEE_DISPLAY_GIFT_REWARD, taskSet: normalizeTaskSet(query.get("taskSet") || "quota"), tasks: { transport: cloneTasks(FALLBACK_TASKS.transport), @@ -979,7 +980,6 @@ function applyLandingData(body) { state.friendName = trimValue(body.inviterNickname) || state.friendName; state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR; state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl; - state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward; const inviteCode = trimValue(body.inviteCode); if (inviteCode) { diff --git a/h5/app-invite/landing.js b/h5/app-invite/landing.js index 22cbb50..f69c270 100644 --- a/h5/app-invite/landing.js +++ b/h5/app-invite/landing.js @@ -6,6 +6,7 @@ const LOCAL_INVITE_API_BASE = "http://127.0.0.1:2900/"; const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/"; const ONLINE_INVITE_API_PREFIX = "/go"; const DEFAULT_AVATAR = "./assets/avatar-sample.png"; +const INVITEE_DISPLAY_GIFT_REWARD = 1000; const state = { apiBase: resolveAPIBase(), @@ -13,7 +14,7 @@ const state = { inviteCode: resolveInviteCode(), friendName: trimValue(query.get("friendName")) || "Name", inviterAvatar: trimValue(query.get("inviterAvatar")) || DEFAULT_AVATAR, - giftReward: Number(query.get("giftReward") || 1000), + giftReward: INVITEE_DISPLAY_GIFT_REWARD, downloadUrl: trimValue(query.get("downloadUrl")), }; @@ -202,7 +203,6 @@ function applyLandingData(body) { state.friendName = trimValue(body.inviterNickname) || state.friendName; state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR; state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl; - state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward; const inviteCode = trimValue(body.inviteCode); if (inviteCode) { diff --git a/h5/assets/room-turnover-reward/hero.png b/h5/assets/room-turnover-reward/hero.png new file mode 100644 index 0000000..45a4958 Binary files /dev/null and b/h5/assets/room-turnover-reward/hero.png differ diff --git a/h5/host-center/index.back.html b/h5/host-center/index.back.html new file mode 100644 index 0000000..bc5a8ca --- /dev/null +++ b/h5/host-center/index.back.html @@ -0,0 +1,111 @@ + + + + + + Host Center + + + +
+ + +
+
My agency
+ +
+
+ + U +
+ + +
+ +
+
+
My available salary
+ +
+
0
+
+ +
+
Today's task
+
+
+
Anchor type
+
Chat
+
+
+
Minutes
+
0/120
+
+
+
Gift Task
+
0
+
+
+
+ + +
+ + + + + + +
+ + + + diff --git a/h5/host-center/index.html b/h5/host-center/index.html index bc5a8ca..13bcf44 100644 --- a/h5/host-center/index.html +++ b/h5/host-center/index.html @@ -1,111 +1,14 @@ - + - - - - Host Center - - - -
- - -
-
My agency
- -
-
- - U -
- - -
- -
-
-
My available salary
- -
-
0
-
- -
-
Today's task
-
-
-
Anchor type
-
Chat
-
-
-
Minutes
-
0/120
-
-
-
Gift Task
-
0
-
-
-
- - -
- - - - - - -
- - - - + + + + Document + + + + + \ No newline at end of file diff --git a/h5/hyapp/host-center/index.html b/h5/hyapp/host-center/index.html new file mode 100644 index 0000000..9870b74 --- /dev/null +++ b/h5/hyapp/host-center/index.html @@ -0,0 +1,125 @@ + + + + + + Stream Center + + + +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
Oscar
+
UID: 1234567890
+ +
+
+ +
+
+
Streaming Duration
(Minutes)
+
0
+
+ +
+
Target
+ +
+
+ +
+
+
+ My Income + +
+ + Withdraw/Exchange + + +
+ + + 0.00 + + +
+ +
+ + + + Anchor Policy + + + +
+
+ + +
+ + + diff --git a/h5/hyapp/host-center/locales/ar.json b/h5/hyapp/host-center/locales/ar.json new file mode 100644 index 0000000..bbdc8e4 --- /dev/null +++ b/h5/hyapp/host-center/locales/ar.json @@ -0,0 +1,27 @@ +{ + "document_title": "مركز البث", + "page_label": "مركز البث", + "back": "رجوع", + "title": "مركز البث", + "language_button_aria": "تغيير اللغة", + "uid": "معرّف المستخدم: 1234567890", + "agency": "الوكالة: Bilibili(101013)", + "streaming_duration": "مدة البث
(بالدقائق)", + "target": "الهدف", + "my_income": "دخلي", + "withdraw_exchange": "سحب/استبدال", + "open_withdraw_exchange": "فتح صفحة السحب والاستبدال", + "open_anchor_policy": "فتح سياسة المضيف", + "anchor_policy": "سياسة المضيف", + "policy_loading": "جار تحميل السياسة...", + "policy_empty": "لا توجد بيانات سياسة", + "policy_missing_token": "رمز الدخول مفقود", + "policy_load_failed": "فشل تحميل السياسة", + "policy_level": "المستوى", + "policy_online": "الاتصال (ساعة)", + "policy_effective_day": "الأيام الصالحة", + "policy_target": "الهدف", + "policy_host_salary": "راتب المضيف", + "policy_agent_salary": "راتب الوكيل", + "policy_total_salary": "إجمالي الراتب" +} diff --git a/h5/hyapp/host-center/locales/en.json b/h5/hyapp/host-center/locales/en.json new file mode 100644 index 0000000..dcf8e4d --- /dev/null +++ b/h5/hyapp/host-center/locales/en.json @@ -0,0 +1,27 @@ +{ + "document_title": "Stream Center", + "page_label": "Stream Center", + "back": "Back", + "title": "Stream Center", + "language_button_aria": "Change language", + "uid": "UID: 1234567890", + "agency": "Agency: Bilibili(101013)", + "streaming_duration": "Streaming Duration
(Minutes)", + "target": "Target", + "my_income": "My Income", + "withdraw_exchange": "Withdraw/Exchange", + "open_withdraw_exchange": "Open withdraw and exchange page", + "open_anchor_policy": "Open anchor policy", + "anchor_policy": "Anchor Policy", + "policy_loading": "Loading policy...", + "policy_empty": "No policy data", + "policy_missing_token": "Missing token", + "policy_load_failed": "Failed to load policy", + "policy_level": "Level", + "policy_online": "Online (Hour)", + "policy_effective_day": "Valid days", + "policy_target": "Target", + "policy_host_salary": "Host salary", + "policy_agent_salary": "Agent salary", + "policy_total_salary": "Total salary" +} diff --git a/h5/hyapp/host-center/locales/id.json b/h5/hyapp/host-center/locales/id.json new file mode 100644 index 0000000..15e2fa3 --- /dev/null +++ b/h5/hyapp/host-center/locales/id.json @@ -0,0 +1,27 @@ +{ + "document_title": "Pusat Streaming", + "page_label": "Pusat Streaming", + "back": "Kembali", + "title": "Pusat Streaming", + "language_button_aria": "Ubah bahasa", + "uid": "UID: 1234567890", + "agency": "Agensi: Bilibili(101013)", + "streaming_duration": "Durasi Streaming
(Menit)", + "target": "Target", + "my_income": "Penghasilan Saya", + "withdraw_exchange": "Tarik/Tukar", + "open_withdraw_exchange": "Buka halaman tarik dan tukar", + "open_anchor_policy": "Buka kebijakan host", + "anchor_policy": "Kebijakan Host", + "policy_loading": "Memuat kebijakan...", + "policy_empty": "Tidak ada data kebijakan", + "policy_missing_token": "Token tidak ditemukan", + "policy_load_failed": "Gagal memuat kebijakan", + "policy_level": "Level", + "policy_online": "Online (Jam)", + "policy_effective_day": "Hari valid", + "policy_target": "Target", + "policy_host_salary": "Gaji host", + "policy_agent_salary": "Gaji agensi", + "policy_total_salary": "Total gaji" +} diff --git a/h5/hyapp/host-center/locales/tr.json b/h5/hyapp/host-center/locales/tr.json new file mode 100644 index 0000000..1413d68 --- /dev/null +++ b/h5/hyapp/host-center/locales/tr.json @@ -0,0 +1,27 @@ +{ + "document_title": "Yayın Merkezi", + "page_label": "Yayın Merkezi", + "back": "Geri", + "title": "Yayın Merkezi", + "language_button_aria": "Dili değiştir", + "uid": "UID: 1234567890", + "agency": "Ajans: Bilibili(101013)", + "streaming_duration": "Yayın Süresi
(Dakika)", + "target": "Hedef", + "my_income": "Gelirim", + "withdraw_exchange": "Çek/Değiştir", + "open_withdraw_exchange": "Çekme ve değişim sayfasını aç", + "open_anchor_policy": "Host politikasını aç", + "anchor_policy": "Host Politikası", + "policy_loading": "Politika yükleniyor...", + "policy_empty": "Politika verisi yok", + "policy_missing_token": "Token eksik", + "policy_load_failed": "Politika yüklenemedi", + "policy_level": "Seviye", + "policy_online": "Online (Saat)", + "policy_effective_day": "Geçerli gün", + "policy_target": "Hedef", + "policy_host_salary": "Host maaşı", + "policy_agent_salary": "Ajans maaşı", + "policy_total_salary": "Toplam maaş" +} diff --git a/h5/hyapp/host-center/original-vue-logic.md b/h5/hyapp/host-center/original-vue-logic.md new file mode 100644 index 0000000..5dd2127 --- /dev/null +++ b/h5/hyapp/host-center/original-vue-logic.md @@ -0,0 +1,266 @@ +# 原 Vue Host Center 逻辑清单 + +来源: +- `chatapp-h5/h5/js/index-CN7d9oZS-1776148658686.js` +- `chatapp-h5/h5/js/pageConfig-BUpLPO7w-1776148658686.js` +- `chatapp-h5/h5/js/teamBill-BWXvrc1S-1776148658686.js` +- `chatapp-h5/h5/js/index-CIAVq8iD-1776148658686.js` +- 后端参数定义参考 `chatapp3-java/rc-service` + +## 页面初始化 + +- 初始化 i18n:读取当前 locale,并调用语言切换逻辑。 +- 初始化路由对象。 +- 初始化 identity store,并设置身份为 `HOST`。 +- 清空 payee store。 +- 使用 `usePageConfig("HOST_CENTER", { needsTeamInfo: true, forceRefresh: true, onDataLoaded })`。 +- 页面 mounted 后并发或顺序触发: + - 设置身份:`identity = "HOST"` + - 获取我的代理资料 + - 清空收款人缓存 + - 获取银行卡列表 + - 获取提现/KYC 信息 + - 获取主播工资提现是否可用 + +## 用户信息获取 + +- `usePageConfig` 内部优先读取缓存用户信息。 +- 非强制刷新时读取本地缓存用户资料。 +- 强制刷新时请求 `/team/member/profile`。 +- 页面展示用字段: + - `id` + - `account` + - `userNickname` + - `name` + - `userAvatar` + - `ownSpecialId.account` +- Host Center 当前代理/团长资料来自 `/team/team-own/profile`。 +- 展示名称:`userNickname || name` +- 展示 ID:`ownSpecialId.account || account` +- 头像加载失败时走 `imageHandler` fallback。 + +## App 连接与请求头 + +- 判断是否在 App/WebView 环境。 +- 若已连接且缓存有效,复用缓存请求头。 +- 若未连接,调用 App bridge 获取 access origin 参数。 +- 解析 App 返回参数后设置 HTTP 请求头。 +- 关键请求头来源: + - `Authorization` + - `Req-Lang` + - `Req-App-Intel` + - `Req-Imei` + - `Req-Sys-Origin` +- 连接失败时可能跳转 `/not_app`。 + +## 路由与身份守卫 + +- 请求 `/app/h5/identity` 获取身份。 +- 根据身份字段计算 roles: + - `freightAgent` + - `agent` + - `bdLeader` + - `bd` + - `anchor` + - `admin` +- 主角色权重: + - `guest` + - `anchor` + - `agent` + - `bd` + - `bdLeader` + - `freightAgent` + - `admin` +- 默认跳转页: + - `admin` -> `/admin-center` + - `freightAgent` -> `/coin-seller` + - `agent` -> `/agency-center` + - `bdLeader` -> `/bd-leader-center` + - `bd` -> `/bd-center` + - `anchor` -> `/host-center` + - `guest` -> `/apply` +- 当前用户没有 anchor 身份时跳到对应默认页。 + +## 收入/工资逻辑 + +- 可用工资来自 `usePageConfig` 的 `salaryInfo.currentSalary`。 +- `usePageConfig` 会请求 `/wallet/bank/balance`。 +- 响应 body 是 number 时展示 `body.toFixed(2)`。 +- 请求失败时 `currentSalary = 0` 并 toast:`Failed to load balance. Please try again.` +- 原 Host Center 还请求 `/team/member-work`,用于获取历史工资和目标数据。 +- `/team/member-work` 返回后: + - `historySalary` 写入本地变量 + - 读取 `targets[0]` + - 结算状态映射: + - `SETTLED` -> `Completed` + - `UNPAID` -> `In Progress` + - `HANG_UP` -> `Out of account` + - `PAY_OUT` -> `Completed` + - 目标等级取 `target.settlementResult.level || target.level || "-"` + +## 今日任务/直播数据逻辑 + +- `usePageConfig` 会请求 `/team/member/work/statistics-now`。 +- 请求参数: + - `dataType=DAILY` + - `memberId=<当前用户 id/account>` +- 展示逻辑: + - `anchorType = "Chat"` + - `minutesProgress = min(ownTime + otherTime, minutesTotal)` + - `minutesTotal = 120` + - `giftTask = acceptGiftValue || 0` + +## 银行卡与提现信息逻辑 + +- 请求 `/wallet/bank-card/list` 获取银行卡列表。 +- 取 `body.find(item => item.use)` 作为当前使用银行卡。 +- 请求 `/wallet/withdraw-info/list` 获取提现/KYC 信息。 +- 若 `body.length > 0`: + - 取第一条 + - `passportFrontUrl` 按 JSON 字符串解析成 `previewUrls` + - 合并到提现信息对象 +- 请求 `/wallet/bank/host-salary/withdraw/enabled`。 +- `body` 转 Boolean 后控制是否展示 `Cash withdraw` 入口。 +- 点击提现: + - 如果没有提现信息或没有当前使用银行卡,打开 `infoMissing` 弹窗。 + - 否则跳转 `/cash-withdraw`。 + +## 弹窗逻辑 + +- 弹窗状态: + - `infoMissing` + - `leave` +- `maskLayerShow = infoMissing || leave` +- 打开弹窗前先关闭其它弹窗。 +- 点击遮罩关闭弹窗。 +- `infoMissing` 弹窗: + - 标题:`prompt` + - 文案:`fill_info_tips` + - 辅助文案:`card_issue_tips` + - 按钮跳转 `/host-setting` +- `leave` 弹窗: + - 标题:`tips` + - 文案:`leave_agent_tips` + - 取消关闭弹窗 + - 确认执行退出团队申请 + +## 退出团队逻辑 + +- 请求 `/team/user/apply` +- 方法:`POST` +- Body: + - `reason: "QUIT"` + - `teamId: <当前团队 id>` +- `teamId` 来源: + - 原 Vue 使用 `pe()` 读取当前团队 id + - 静态实现里曾兼容读取 `teamInfo.id || agency.teamId || agency.id` +- 失败时 toast `errorMsg`。 + +## 点击跳转 + +- 工资详情:`/history-salary` +- 平台政策:`/platform-policy?from=hostcenter` +- 主播设置/银行卡:`/host-setting` +- 兑换金币:`/exchange-gold-coins` +- 转账:`/transfer` +- 提现:`/cash-withdraw` +- 语言切换:`/language` +- 返回: + - App bridge `closePage` + - Flutter bridge `close_page` + - 浏览器 `history.back()` + +## 原接口与可传参数 + +### GET `/team/member/profile` + +- 用途:获取当前用户信息。 +- 显式查询参数:无。 +- 隐式参数:App 请求头中用户、语言、来源等上下文。 + +### GET `/team/team-own/profile` + +- 用途:获取我的团长/代理资料。 +- 显式查询参数:无。 +- 隐式参数:App 请求头中的当前用户上下文。 + +### GET `/team/entry` + +- 用途:获取当前用户团队入口信息。 +- 显式查询参数:无。 +- 返回中使用 `body.teamProfile || body` 作为团队信息。 + +### GET `/wallet/bank/balance` + +- 用途:获取工资/银行卡余额。 +- 显式查询参数:无。 +- 返回可为数字,也可能为对象。 + +### GET `/team/member/work/statistics-now` + +- 用途:获取成员当天目标/工作统计。 +- 查询参数: + - `dataType`: `DAILY | MONTH` + - `memberId`: 成员 ID,必传 + - `size`: 数量,可选 +- Host Center 使用: + - `dataType=DAILY` + - `memberId=<当前用户 id/account>` + +### GET `/team/member-work` + +- 用途:成员工资情况。 +- 查询参数: + - `userId`: 用户 ID,必传 +- 返回使用: + - `historySalary` + - `targets` + - `targets[].status` + - `targets[].target.settlementResult.level` + - `targets[].target.level` + +### GET `/wallet/bank-card/list` + +- 用途:银行卡列表。 +- 显式查询参数:无。 +- 返回使用: + - `body[]` + - `item.use` + +### GET `/wallet/withdraw-info/list` + +- 用途:提现/KYC 信息列表。 +- 显式查询参数:无。 +- 返回使用: + - `body[0]` + - `body[0].passportFrontUrl` + +### GET `/wallet/bank/host-salary/withdraw/enabled` + +- 用途:主播工资是否允许提现。 +- 显式查询参数:无。 +- 返回使用: + - `body` boolean + +### GET `/app/h5/identity` + +- 用途:获取当前 H5 身份。 +- 显式查询参数:无。 +- 返回使用字段: + - `freightAgent` + - `agent` + - `bdLeader` + - `bd` + - `anchor` + - `admin` + +### POST `/team/user/apply` + +- 用途:用户申请加入或退出团队。 +- Body: + - `reason`: `JOIN | QUIT` + - `teamId`: 团队 ID,必传 + - `url`: 跳转 URL,可选 +- Host Center 退出团队使用: + - `reason: "QUIT"` + - `teamId: <当前团队 id>` diff --git a/h5/hyapp/host-center/policy.html b/h5/hyapp/host-center/policy.html new file mode 100644 index 0000000..6ed1224 --- /dev/null +++ b/h5/hyapp/host-center/policy.html @@ -0,0 +1,55 @@ + + + + + + Anchor Policy + + + +
+ + + + + + +
+
+
+

Anchor Policy

+ +
+
Loading policy...
+ +
+
+ + +
+ + + diff --git a/h5/hyapp/host-center/policy.js b/h5/hyapp/host-center/policy.js new file mode 100644 index 0000000..39d9ed0 --- /dev/null +++ b/h5/hyapp/host-center/policy.js @@ -0,0 +1,375 @@ +(function () { + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + const API_BASE_URL = "https://jvapi.haiyihy.com"; + + const fallbackMessages = { + en: { + document_title: "Anchor Policy", + back: "Back", + language_button_aria: "Change language", + anchor_policy: "Anchor Policy", + policy_loading: "Loading policy...", + policy_empty: "No policy data", + policy_missing_token: "Missing token", + policy_load_failed: "Failed to load policy", + policy_level: "Level", + policy_online: "Online (Hour)", + policy_effective_day: "Valid days", + policy_target: "Target", + policy_host_salary: "Host salary", + policy_agent_salary: "Agent salary", + policy_total_salary: "Total salary" + }, + ar: { + document_title: "سياسة المضيف", + back: "رجوع", + language_button_aria: "تغيير اللغة", + anchor_policy: "سياسة المضيف", + policy_loading: "جار تحميل السياسة...", + policy_empty: "لا توجد بيانات سياسة", + policy_missing_token: "رمز الدخول مفقود", + policy_load_failed: "فشل تحميل السياسة", + policy_level: "المستوى", + policy_online: "الاتصال (ساعة)", + policy_effective_day: "الأيام الصالحة", + policy_target: "الهدف", + policy_host_salary: "راتب المضيف", + policy_agent_salary: "راتب الوكيل", + policy_total_salary: "إجمالي الراتب" + }, + tr: { + document_title: "Host Politikası", + back: "Geri", + language_button_aria: "Dili değiştir", + anchor_policy: "Host Politikası", + policy_loading: "Politika yükleniyor...", + policy_empty: "Politika verisi yok", + policy_missing_token: "Token eksik", + policy_load_failed: "Politika yüklenemedi", + policy_level: "Seviye", + policy_online: "Online (Saat)", + policy_effective_day: "Geçerli gün", + policy_target: "Hedef", + policy_host_salary: "Host maaşı", + policy_agent_salary: "Ajans maaşı", + policy_total_salary: "Toplam maaş" + }, + id: { + document_title: "Kebijakan Host", + back: "Kembali", + language_button_aria: "Ubah bahasa", + anchor_policy: "Kebijakan Host", + policy_loading: "Memuat kebijakan...", + policy_empty: "Tidak ada data kebijakan", + policy_missing_token: "Token tidak ditemukan", + policy_load_failed: "Gagal memuat kebijakan", + policy_level: "Level", + policy_online: "Online (Jam)", + policy_effective_day: "Hari valid", + policy_target: "Target", + policy_host_salary: "Gaji host", + policy_agent_salary: "Gaji agensi", + policy_total_salary: "Total gaji" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en; + let currentPolicy = null; + let policyStatusKey = "policy_loading"; + + 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(); + return supportedLanguages.includes(value) ? value : "en"; + } + + async function loadMessages(lang) { + const normalizedLang = normalizeLanguage(lang); + if (window.location.protocol === "file:") return fallbackMessages[normalizedLang]; + + try { + const response = await fetch(`./locales/${normalizedLang}.json`, { cache: "no-store" }); + if (response.ok) return await response.json(); + } catch (error) { + return fallbackMessages[normalizedLang]; + } + + return fallbackMessages[normalizedLang]; + } + + function message(key, fallback) { + return currentMessages[key] || fallbackMessages.en[key] || fallback || key; + } + + function renderMessages(messages) { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = messages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = messages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", 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 renderPolicyStatus(key, fallback) { + policyStatusKey = key; + const status = document.querySelector("#policyStatus"); + const list = document.querySelector("#policyList"); + const region = document.querySelector("#policyRegion"); + if (status) { + status.textContent = message(key, fallback); + status.hidden = false; + } + if (list) { + list.hidden = true; + list.replaceChildren(); + } + if (region) region.textContent = ""; + } + + function formatPlainValue(value) { + if (value === null || value === undefined || value === "") return "0"; + return String(value); + } + + function formatTarget(value) { + const number = Number(value); + if (!Number.isFinite(number)) return formatPlainValue(value); + if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`; + if (number >= 1000) return `${(number / 1000).toFixed(1)}K`; + return String(number); + } + + function formatSalary(value, policyType) { + const text = formatPlainValue(value); + return policyType === "SALARY_DIAMOND" ? text : `$${text}`; + } + + function createPolicyCell(label, value) { + const cell = document.createElement("div"); + cell.className = "policy-cell"; + + const labelNode = document.createElement("div"); + labelNode.className = "policy-cell-label"; + labelNode.textContent = label; + + const valueNode = document.createElement("div"); + valueNode.className = "policy-cell-value"; + valueNode.textContent = value; + + cell.append(labelNode, valueNode); + return cell; + } + + function createPolicyRow(item, policyType) { + const row = document.createElement("article"); + row.className = "policy-row"; + + const head = document.createElement("div"); + head.className = "policy-row-head"; + + const level = document.createElement("div"); + level.className = "policy-level"; + level.textContent = `${message("policy_level")} ${formatPlainValue(item.level)}`; + + const effectiveDay = document.createElement("div"); + effectiveDay.className = "policy-effective-day"; + effectiveDay.textContent = `${message("policy_effective_day")}: ${formatPlainValue(item.effectiveDay)}`; + head.append(level, effectiveDay); + + const grid = document.createElement("div"); + grid.className = "policy-grid"; + grid.append( + createPolicyCell(message("policy_online"), formatPlainValue(item.onlineTime)), + createPolicyCell(message("policy_target"), formatTarget(item.target)), + createPolicyCell(message("policy_host_salary"), formatSalary(item.memberSalary, policyType)), + createPolicyCell(message("policy_agent_salary"), formatSalary(item.ownSalary, policyType)), + createPolicyCell(message("policy_total_salary"), formatSalary(item.totalSalary, policyType)) + ); + + row.append(head, grid); + return row; + } + + function renderAnchorPolicy(policy) { + const rows = Array.isArray(policy?.policy) ? policy.policy : []; + const list = document.querySelector("#policyList"); + const status = document.querySelector("#policyStatus"); + const region = document.querySelector("#policyRegion"); + if (!list || !status) return; + + if (region) { + region.textContent = [policy?.regionCode || policy?.region, policy?.countryCode].filter(Boolean).join(" · "); + } + + if (!rows.length || policy?.empty) { + renderPolicyStatus("policy_empty"); + return; + } + + status.hidden = true; + list.hidden = false; + list.replaceChildren(...rows.map((item) => createPolicyRow(item, policy?.policyType))); + } + + async function fetchAnchorPolicy() { + renderPolicyStatus("policy_loading"); + + const token = readRawParam("token"); + if (!token) { + renderPolicyStatus("policy_missing_token"); + return; + } + + try { + const url = new URL("/app/h5/host-center/policy", `${apiBaseURL()}/`); + url.searchParams.set("token", token); + if (params.get("policyType")) url.searchParams.set("policyType", params.get("policyType")); + + const response = await fetch(url.toString(), { cache: "no-store" }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + currentPolicy = data.body || data.data || {}; + renderAnchorPolicy(currentPolicy); + } catch (error) { + console.error("Failed to load anchor policy:", error); + currentPolicy = null; + renderPolicyStatus("policy_load_failed", error.message); + } + } + + function syncBackFallback() { + const backButton = document.querySelector(".back-button"); + if (!backButton) return; + + backButton.addEventListener("click", () => { + if (window.history.length > 1) { + window.history.back(); + return; + } + + const nextParams = new URLSearchParams(params); + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + const query = nextParams.toString(); + window.location.href = `./index.html${query ? `?${query}` : ""}`; + }); + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + function renderLanguageControls(messages) { + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", messages.language_button_aria || "Change language"); + } + + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + const isActive = button.dataset.lang === currentLang; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); + } + + async function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + const messages = await loadMessages(currentLang); + currentMessages = messages; + + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = messages.document_title || messages.anchor_policy || "Anchor Policy"; + renderMessages(messages); + renderLanguageControls(messages); + if (currentPolicy) { + renderAnchorPolicy(currentPolicy); + } else { + renderPolicyStatus(policyStatusKey); + } + } + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.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); + }); + + syncBackFallback(); + applyLanguage(currentLang).then(fetchAnchorPolicy); +})(); diff --git a/h5/hyapp/host-center/script.js b/h5/hyapp/host-center/script.js new file mode 100644 index 0000000..b957384 --- /dev/null +++ b/h5/hyapp/host-center/script.js @@ -0,0 +1,632 @@ +(function () { + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + const API_BASE_URL = "https://jvapi.haiyihy.com"; + + const fallbackMessages = { + en: { + document_title: "Stream Center", + page_label: "Stream Center", + back: "Back", + title: "Stream Center", + language_button_aria: "Change language", + uid: "UID: 1234567890", + agency: "Agency: Bilibili(101013)", + streaming_duration: "Streaming Duration
(Minutes)", + target: "Target", + my_income: "My Income", + withdraw_exchange: "Withdraw/Exchange", + open_withdraw_exchange: "Open withdraw and exchange page", + open_anchor_policy: "Open anchor policy", + anchor_policy: "Anchor Policy", + policy_loading: "Loading policy...", + policy_empty: "No policy data", + policy_missing_token: "Missing token", + policy_load_failed: "Failed to load policy", + policy_level: "Level", + policy_online: "Online (Hour)", + policy_effective_day: "Valid days", + policy_target: "Target", + policy_host_salary: "Host salary", + policy_agent_salary: "Agent salary", + policy_total_salary: "Total salary" + }, + ar: { + document_title: "مركز البث", + page_label: "مركز البث", + back: "رجوع", + title: "مركز البث", + language_button_aria: "تغيير اللغة", + uid: "معرّف المستخدم: 1234567890", + agency: "الوكالة: Bilibili(101013)", + streaming_duration: "مدة البث
(بالدقائق)", + target: "الهدف", + my_income: "دخلي", + withdraw_exchange: "سحب/استبدال", + open_withdraw_exchange: "فتح صفحة السحب والاستبدال", + open_anchor_policy: "فتح سياسة المضيف", + anchor_policy: "سياسة المضيف", + policy_loading: "جار تحميل السياسة...", + policy_empty: "لا توجد بيانات سياسة", + policy_missing_token: "رمز الدخول مفقود", + policy_load_failed: "فشل تحميل السياسة", + policy_level: "المستوى", + policy_online: "الاتصال (ساعة)", + policy_effective_day: "الأيام الصالحة", + policy_target: "الهدف", + policy_host_salary: "راتب المضيف", + policy_agent_salary: "راتب الوكيل", + policy_total_salary: "إجمالي الراتب" + }, + tr: { + document_title: "Yayın Merkezi", + page_label: "Yayın Merkezi", + back: "Geri", + title: "Yayın Merkezi", + language_button_aria: "Dili değiştir", + uid: "UID: 1234567890", + agency: "Ajans: Bilibili(101013)", + streaming_duration: "Yayın Süresi
(Dakika)", + target: "Hedef", + my_income: "Gelirim", + withdraw_exchange: "Çek/Değiştir", + open_withdraw_exchange: "Çekme ve değişim sayfasını aç", + open_anchor_policy: "Host politikasını aç", + anchor_policy: "Host Politikası", + policy_loading: "Politika yükleniyor...", + policy_empty: "Politika verisi yok", + policy_missing_token: "Token eksik", + policy_load_failed: "Politika yüklenemedi", + policy_level: "Seviye", + policy_online: "Online (Saat)", + policy_effective_day: "Geçerli gün", + policy_target: "Hedef", + policy_host_salary: "Host maaşı", + policy_agent_salary: "Ajans maaşı", + policy_total_salary: "Toplam maaş" + }, + id: { + document_title: "Pusat Streaming", + page_label: "Pusat Streaming", + back: "Kembali", + title: "Pusat Streaming", + language_button_aria: "Ubah bahasa", + uid: "UID: 1234567890", + agency: "Agensi: Bilibili(101013)", + streaming_duration: "Durasi Streaming
(Menit)", + target: "Target", + my_income: "Penghasilan Saya", + withdraw_exchange: "Tarik/Tukar", + open_withdraw_exchange: "Buka halaman tarik dan tukar", + open_anchor_policy: "Buka kebijakan host", + anchor_policy: "Kebijakan Host", + policy_loading: "Memuat kebijakan...", + policy_empty: "Tidak ada data kebijakan", + policy_missing_token: "Token tidak ditemukan", + policy_load_failed: "Gagal memuat kebijakan", + policy_level: "Level", + policy_online: "Online (Jam)", + policy_effective_day: "Hari valid", + policy_target: "Target", + policy_host_salary: "Gaji host", + policy_agent_salary: "Gaji agensi", + policy_total_salary: "Total gaji" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMessages = fallbackMessages[currentLang] || fallbackMessages.en; + let currentProfile = null; + let currentSalary = "0.00"; + let currentStreamingDuration = "0"; + let nextTargetValue = "-"; + + 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(); + return supportedLanguages.includes(value) ? value : "en"; + } + + async function loadMessages(lang) { + const normalizedLang = normalizeLanguage(lang); + if (window.location.protocol === "file:") return fallbackMessages[normalizedLang]; + + try { + const response = await fetch(`./locales/${normalizedLang}.json`, { cache: "no-store" }); + if (response.ok) return await response.json(); + } catch (error) { + return fallbackMessages[normalizedLang]; + } + + return fallbackMessages[normalizedLang]; + } + + function renderMessages(messages) { + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = messages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + + document.querySelectorAll("[data-i18n-html]").forEach((node) => { + const value = messages[node.dataset.i18nHtml]; + if (value) node.innerHTML = value; + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = messages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", 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" + }; + } + + function appendTokenParam(url) { + const token = readRawParam("token"); + if (!token) return false; + url.searchParams.set("token", token); + return true; + } + + function profileUidPrefix(messages) { + const template = messages.uid || fallbackMessages.en.uid; + const marker = "1234567890"; + return template.includes(marker) ? template.replace(marker, "").trim() : "UID:"; + } + + function pickProfileName(profile) { + return profile?.userNickname || profile?.name || "Oscar"; + } + + function pickProfileAccount(profile) { + return profile?.ownSpecialId?.account || profile?.account || profile?.id || "1234567890"; + } + + function renderProfile(profile, messages = currentMessages) { + if (!profile) return; + + const profileCard = document.querySelector(".profile-card"); + const nameNode = document.querySelector("#profileName"); + const uidNode = document.querySelector("#profileUid"); + const avatarImage = document.querySelector("#profileAvatar"); + const avatarFallback = document.querySelector("#profileAvatarFallback"); + const displayName = pickProfileName(profile); + const displayAccount = pickProfileAccount(profile); + + if (profileCard) { + profileCard.dataset.userId = String(profile.id || ""); + profileCard.dataset.account = String(profile.account || ""); + profileCard.dataset.specialAccount = String(profile?.ownSpecialId?.account || ""); + } + + if (nameNode) nameNode.textContent = displayName; + if (uidNode) uidNode.textContent = `${profileUidPrefix(messages)} ${displayAccount}`.trim(); + + if (!avatarImage || !avatarFallback) return; + + const avatar = profile.userAvatar ? String(profile.userAvatar) : ""; + avatarImage.alt = displayName; + avatarFallback.setAttribute("aria-label", `${displayName} avatar`); + + if (!avatar) { + avatarImage.hidden = true; + avatarFallback.hidden = false; + avatarImage.removeAttribute("src"); + return; + } + + avatarImage.hidden = false; + avatarFallback.hidden = true; + avatarImage.onerror = () => { + avatarImage.hidden = true; + avatarFallback.hidden = false; + avatarImage.removeAttribute("src"); + }; + avatarImage.src = avatar; + } + + function formatCurrentSalary(value) { + const numericValue = Number(value); + if (Number.isFinite(numericValue)) return numericValue.toFixed(2); + return "0.00"; + } + + function pickCurrentSalary(body) { + if (typeof body === "number") return body; + if (body && typeof body === "object") { + return body.currentSalary ?? body.availableBalance ?? body.balance ?? 0; + } + return 0; + } + + function renderCurrentSalary(value) { + currentSalary = formatCurrentSalary(value); + const salaryNode = document.querySelector("#currentSalary"); + if (salaryNode) salaryNode.textContent = currentSalary; + } + + function formatStreamingDuration(value) { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) return "0"; + const safeValue = Math.max(0, Math.min(numericValue, 120)); + return Number.isInteger(safeValue) ? String(safeValue) : String(Number(safeValue.toFixed(2))); + } + + function pickStreamingDuration(body) { + const ownTime = Number(body?.ownTime) || 0; + const otherTime = Number(body?.otherTime) || 0; + return ownTime + otherTime; + } + + function renderStreamingDuration(value) { + currentStreamingDuration = formatStreamingDuration(value); + const durationNode = document.querySelector("#streamingDuration"); + if (durationNode) durationNode.textContent = currentStreamingDuration; + } + + function formatTargetValue(value) { + if (value === null || value === undefined || value === "") return "-"; + const number = Number(value); + if (Number.isFinite(number) && Number.isInteger(number)) return String(number); + return String(value); + } + + function renderNextTargetValue(value) { + nextTargetValue = formatTargetValue(value); + const targetNode = document.querySelector("#nextTargetValue"); + if (targetNode) targetNode.textContent = nextTargetValue; + } + + function pickFirstMemberWorkTarget(body) { + const targets = Array.isArray(body?.targets) ? body.targets : []; + return targets[0] || body?.target || null; + } + + function pickGiftValue(workTarget) { + const target = workTarget?.target || workTarget || {}; + return target.acceptGiftValue ?? target.giftValue ?? target.giftScore ?? target.currentGiftValue ?? null; + } + + function pickDirectNextTargetValue(workTarget) { + const target = workTarget?.target || workTarget || {}; + return target.nextTarget ?? target.nextLevelTarget ?? target.nextTargetValue ?? workTarget?.nextTarget ?? null; + } + + function pickPolicyRows(policyBody) { + if (Array.isArray(policyBody?.policy)) return policyBody.policy; + if (Array.isArray(policyBody?.policies)) return policyBody.policies; + return []; + } + + function calculateNextTargetValue(giftValue, policyRows) { + const giftNumber = Number(giftValue); + if (!Number.isFinite(giftNumber) || !policyRows.length) return null; + + const rows = policyRows + .map((row) => ({ + target: Number(row.target), + rawTarget: row.target + })) + .filter((row) => Number.isFinite(row.target)) + .sort((a, b) => a.target - b.target); + + if (!rows.length) return null; + + const next = rows.find((row) => giftNumber < row.target); + return next ? next.rawTarget : null; + } + + async function fetchUserProfile() { + const token = readRawParam("token"); + if (!token) { + console.warn("Missing token for /team/member/profile"); + return null; + } + + try { + const url = new URL("/team/member/profile", `${apiBaseURL()}/`); + + const response = await fetch(url.toString(), { + cache: "no-store", + headers: authHeadersFromToken(token) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + currentProfile = data.body || data.data || {}; + renderProfile(currentProfile); + return currentProfile; + } catch (error) { + console.error("Failed to load user profile:", error); + return null; + } + } + + async function fetchBankBalance() { + const token = readRawParam("token"); + if (!token) { + console.warn("Missing token for /wallet/bank/balance"); + return; + } + + try { + const url = new URL("/wallet/bank/balance", `${apiBaseURL()}/`); + + const response = await fetch(url.toString(), { + cache: "no-store", + headers: authHeadersFromToken(token) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + renderCurrentSalary(pickCurrentSalary(data.body ?? data.data)); + } catch (error) { + console.error("Failed to load bank balance:", error); + renderCurrentSalary(0); + } + } + + async function fetchStreamingDuration(memberId) { + const token = readRawParam("token"); + if (!token) { + console.warn("Missing token for /team/member/work/statistics-now"); + return; + } + if (!memberId) { + console.warn("Missing memberId for /team/member/work/statistics-now"); + return; + } + + try { + const url = new URL("/team/member/work/statistics-now", `${apiBaseURL()}/`); + url.searchParams.set("dataType", "DAILY"); + url.searchParams.set("memberId", String(memberId)); + + const response = await fetch(url.toString(), { + cache: "no-store", + headers: authHeadersFromToken(token) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + renderStreamingDuration(pickStreamingDuration(data.body ?? data.data)); + } catch (error) { + console.error("Failed to load streaming duration:", error); + renderStreamingDuration(0); + } + } + + async function fetchMemberWork(memberId) { + if (!memberId) { + console.warn("Missing memberId for /team/member-work"); + return null; + } + + try { + const url = new URL("/team/member-work", `${apiBaseURL()}/`); + url.searchParams.set("userId", String(memberId)); + const token = readRawParam("token"); + if (!token) { + console.warn("Missing token for /team/member-work"); + return null; + } + + const response = await fetch(url.toString(), { + cache: "no-store", + headers: authHeadersFromToken(token) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + return data.body || data.data || null; + } catch (error) { + console.error("Failed to load member work:", error); + return null; + } + } + + async function fetchAnchorPolicyForTarget() { + try { + const url = new URL("/app/h5/host-center/policy", `${apiBaseURL()}/`); + if (!appendTokenParam(url)) { + console.warn("Missing token for /app/h5/host-center/policy"); + return null; + } + if (params.get("policyType")) url.searchParams.set("policyType", params.get("policyType")); + + const response = await fetch(url.toString(), { cache: "no-store" }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + + return data.body || data.data || null; + } catch (error) { + console.error("Failed to load anchor policy for next target:", error); + return null; + } + } + + async function fetchNextTargetValue(memberId) { + const memberWork = await fetchMemberWork(memberId); + const workTarget = pickFirstMemberWorkTarget(memberWork); + if (!workTarget) { + renderNextTargetValue("-"); + return; + } + + const directNextTarget = pickDirectNextTargetValue(workTarget); + const giftValue = pickGiftValue(workTarget); + const policy = await fetchAnchorPolicyForTarget(); + const calculatedNextTarget = calculateNextTargetValue(giftValue, pickPolicyRows(policy)); + + renderNextTargetValue(calculatedNextTarget ?? directNextTarget ?? "-"); + } + + function syncLocalizedLinks() { + const nextParams = new URLSearchParams(params); + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + const query = nextParams.toString(); + + document.querySelectorAll('a[href^="../withdraw-exchange/index.html"]').forEach((link) => { + link.setAttribute("href", `../withdraw-exchange/index.html${query ? `?${query}` : ""}`); + }); + } + + function syncPolicyLink() { + const link = document.querySelector("#anchorPolicyLink"); + if (!link) return; + + const nextParams = new URLSearchParams(params); + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + + const query = nextParams.toString(); + link.setAttribute("href", `./policy.html${query ? `?${query}` : ""}`); + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + function renderLanguageControls(messages) { + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", messages.language_button_aria || "Change language"); + } + + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + const isActive = button.dataset.lang === currentLang; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); + } + + async function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + const messages = await loadMessages(currentLang); + currentMessages = messages; + + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.title = messages.document_title || messages.title || "Stream Center"; + renderMessages(messages); + renderLanguageControls(messages); + syncLocalizedLinks(); + syncPolicyLink(); + renderProfile(currentProfile, messages); + } + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.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(".back-button")?.addEventListener("click", () => { + if (window.history.length > 1) window.history.back(); + }); + + applyLanguage(currentLang).then(() => { + fetchUserProfile().then((profile) => { + const memberId = profile?.id || profile?.account; + if (memberId) { + fetchStreamingDuration(memberId); + fetchNextTargetValue(memberId); + } + }); + fetchBankBalance(); + }); +})(); diff --git a/h5/hyapp/host-center/style.css b/h5/hyapp/host-center/style.css new file mode 100644 index 0000000..5e973ac --- /dev/null +++ b/h5/hyapp/host-center/style.css @@ -0,0 +1,791 @@ +:root { + --page-bg: #f6f7f7; + --card-bg: rgba(255, 255, 255, 0.96); + --text: #25282e; + --muted: #8f9196; + --line: #eef0f2; + --mint: #43e7d8; + --gold: #e3b55b; + --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 { + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +.host-center { + position: relative; + width: 100%; + max-width: 430px; + min-height: 100vh; + overflow: hidden; + background: var(--page-bg); +} + +.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%); +} + +.status-bar, +.title-bar, +.content, +.home-indicator { + position: relative; + z-index: 1; +} + +.status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 47px; + padding: 18px 29px 0 38px; + color: #11161c; + direction: ltr; + font-size: 15px; + font-weight: 800; +} + +.status-icons { + display: flex; + align-items: center; + gap: 6px; +} + +.cellular { + display: flex; + align-items: flex-end; + gap: 2px; + width: 18px; + height: 12px; +} + +.cellular i { + width: 3px; + border-radius: 2px; + background: #11161c; +} + +.cellular i:nth-child(1) { + height: 4px; +} + +.cellular i:nth-child(2) { + height: 6px; +} + +.cellular i:nth-child(3) { + height: 9px; +} + +.cellular i:nth-child(4) { + height: 11px; +} + +.wifi { + position: relative; + width: 18px; + height: 13px; +} + +.wifi i { + position: absolute; + left: 50%; + border: 2px solid #11161c; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; + border-radius: 12px 12px 0 0; + transform: translateX(-50%); +} + +.wifi i:nth-child(1) { + top: 1px; + width: 16px; + height: 8px; +} + +.wifi i:nth-child(2) { + top: 5px; + width: 10px; + height: 5px; +} + +.wifi i:nth-child(3) { + top: 10px; + width: 4px; + height: 4px; + border: 0; + border-radius: 50%; + background: #11161c; +} + +.battery { + position: relative; + width: 24px; + height: 12px; + border: 2px solid #11161c; + border-radius: 3px; +} + +.battery::after { + content: ""; + position: absolute; + top: 3px; + right: -4px; + width: 2px; + height: 4px; + border-radius: 0 2px 2px 0; + background: #11161c; +} + +.battery i { + display: block; + width: 16px; + height: 100%; + background: #11161c; +} + +.title-bar { + display: grid; + grid-template-columns: 52px 1fr 52px; + align-items: center; + z-index: 3; + height: 60px; + padding: 0 14px; + direction: ltr; +} + +.title-bar h1 { + margin: 0; + color: #24282e; + font-size: 22px; + font-weight: 900; + line-height: 1; + text-align: center; +} + +.back-button { + display: flex; + align-items: center; + justify-content: flex-start; + width: 44px; + height: 44px; +} + +.back-button svg { + width: 28px; + height: 28px; +} + +.back-button path { + fill: none; + stroke: #24282e; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 3; +} + +.language-switcher { + position: relative; + justify-self: end; +} + +.language-button { + justify-self: end; + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 30px; + border: 1px solid rgba(35, 40, 46, 0.12); + border-radius: 15px; + background: rgba(255, 255, 255, 0.68); + color: #25282e; + font-size: 13px; + font-weight: 900; + box-shadow: 0 4px 12px rgba(43, 118, 107, 0.08); +} + +.language-menu { + position: absolute; + z-index: 4; + top: calc(100% + 7px); + right: 0; + display: grid; + gap: 2px; + min-width: 58px; + padding: 5px; + border: 1px solid rgba(35, 40, 46, 0.1); + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 10px 26px rgba(36, 46, 60, 0.14); +} + +.language-menu button { + min-height: 28px; + border-radius: 6px; + color: #25282e; + font-size: 12px; + font-weight: 900; +} + +.language-menu button.is-active { + background: #e6fbf8; + color: #0e8f82; +} + +.language-menu[hidden] { + display: none; +} + +.content { + display: flex; + flex-direction: column; + gap: 15px; + padding: 8px 14px 90px; +} + +.policy-content { + padding-top: 18px; +} + +.card { + width: 100%; + border-radius: 8px; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.profile-card { + display: flex; + align-items: center; + min-height: 103px; + padding: 16px 24px; +} + +.avatar-shell { + position: relative; + width: 64px; + height: 64px; + flex: 0 0 64px; +} + +.avatar-image, +.avatar-fallback { + display: block; + width: 64px; + height: 64px; + border-radius: 50%; +} + +.avatar-image { + object-fit: cover; +} + +.avatar-image[hidden], +.avatar-fallback[hidden] { + display: none; +} + +.profile-copy { + min-width: 0; + margin-left: 20px; +} + +.name { + color: #25282e; + font-size: 20px; + font-weight: 900; + line-height: 1.1; +} + +.meta, +.agency { + color: var(--muted); + font-size: 13px; + font-weight: 750; + line-height: 1.2; +} + +.meta { + margin-top: 6px; +} + +.agency { + display: flex; + align-items: center; + max-width: 100%; + margin-top: 6px; +} + +.agency span:first-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chevron { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + color: #8b8e94; + font-size: 25px; + font-weight: 500; + line-height: 0.8; +} + +.metrics-card { + display: grid; + grid-template-columns: 1fr 1px 1fr; + min-height: 111px; + padding: 23px 24px 20px; +} + +.metric { + min-width: 0; +} + +.metric:first-child { + padding-right: 20px; +} + +.gift-metric { + padding-left: 25px; +} + +.vertical-line { + width: 1px; + background: var(--line); +} + +.label { + min-height: 36px; + color: #8b8d92; + font-size: 16px; + font-weight: 750; + line-height: 1.12; +} + +.value { + margin-top: 4px; + color: #292d33; + font-size: 25px; + font-weight: 900; + line-height: 1; +} + +.gift-value, +.income-value { + display: flex; + align-items: center; +} + +.gift-value { + margin-top: 7px; + color: #252a30; + font-size: 24px; + font-weight: 900; + line-height: 1; +} + +.gift-value .amount-text { + min-width: 0; + overflow-wrap: anywhere; +} + +.dollar-symbol { + display: inline-flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + flex: 0 0 25px; + margin-right: 8px; + border-radius: 50%; + background: linear-gradient(145deg, #ffe179 0%, #ffb62d 52%, #f49b20 100%); + color: #fff; + font-size: 18px; + font-weight: 950; + line-height: 1; + box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.7), 0 1px 2px rgba(195, 117, 12, 0.24); +} + +.income-card { + min-height: 143px; + padding: 19px 24px 18px; +} + +.income-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.income-title { + display: flex; + align-items: center; + min-width: 0; + color: #24282e; + font-size: 17px; + font-weight: 900; +} + +.income-title svg { + width: 17px; + height: 17px; + margin-left: 7px; +} + +.income-title circle, +.income-title path { + fill: none; + stroke: #272b31; + stroke-linecap: round; + stroke-width: 2; +} + +.cash-link { + display: flex; + align-items: center; + flex: 0 0 auto; + color: var(--mint); + font-size: 13px; + font-weight: 900; +} + +.cash-link .chevron { + margin-left: 5px; + color: var(--mint); + font-size: 20px; +} + +.income-value { + margin-top: 14px; + max-width: 100%; + color: #23272d; + font-size: 34px; + font-weight: 950; + line-height: 1.05; +} + +.income-currency { + flex: 0 0 auto; + margin-right: 6px; + color: var(--gold); + font-size: 28px; + font-weight: 950; + line-height: 1; +} + +.income-value .amount-text { + min-width: 0; + overflow-wrap: anywhere; +} + +.income-value .chevron { + margin-left: 9px; + color: #b4b6ba; + font-size: 24px; +} + +.policy-entry-card { + min-height: 64px; + padding: 0 16px; +} + +.policy-entry-link { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 64px; +} + +.policy-entry-left { + display: flex; + align-items: center; + min-width: 0; +} + +.policy-entry-left svg { + width: 24px; + height: 24px; + flex: 0 0 24px; + margin-right: 18px; +} + +.policy-entry-left svg path { + fill: none; + stroke: #55585f; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.policy-entry-left span { + min-width: 0; + overflow: hidden; + color: #464951; + font-size: 16px; + font-weight: 850; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.policy-entry-link > .chevron { + flex: 0 0 auto; + margin-left: 12px; + color: #8b8d92; + font-size: 24px; +} + +.policy-card { + min-height: 184px; + padding: 18px 16px 16px; +} + +.policy-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 13px; +} + +.policy-head h2 { + min-width: 0; + margin: 0; + overflow: hidden; + color: #24282e; + font-size: 17px; + font-weight: 900; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.policy-region { + flex: 0 1 auto; + overflow: hidden; + color: #8b8d92; + font-size: 12px; + font-weight: 800; + line-height: 1.2; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.policy-status { + display: flex; + align-items: center; + justify-content: center; + min-height: 112px; + border: 1px dashed #e5e8ea; + border-radius: 8px; + color: #8b8d92; + font-size: 14px; + font-weight: 800; + text-align: center; +} + +.policy-list { + display: grid; + gap: 10px; +} + +.policy-row { + display: grid; + gap: 10px; + padding: 12px; + border: 1px solid #eef0f2; + border-radius: 8px; + background: #fbfcfc; +} + +.policy-row-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.policy-level { + color: #24282e; + font-size: 16px; + font-weight: 950; +} + +.policy-effective-day { + color: #8b8d92; + font-size: 12px; + font-weight: 850; +} + +.policy-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.policy-cell { + min-width: 0; +} + +.policy-cell-label { + overflow: hidden; + color: #96989d; + font-size: 11px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.policy-cell-value { + margin-top: 4px; + overflow-wrap: anywhere; + color: #292d33; + font-size: 14px; + font-weight: 900; + line-height: 1.2; +} + +.home-indicator { + position: absolute; + left: 50%; + bottom: 8px; + width: 154px; + height: 5px; + border-radius: 999px; + background: #050506; + transform: translateX(-50%); +} + +[dir="rtl"] .content { + direction: rtl; +} + +[dir="rtl"] .profile-copy { + margin-right: 20px; + margin-left: 0; + text-align: right; +} + +[dir="rtl"] .income-title svg { + margin-right: 7px; + margin-left: 0; +} + +[dir="rtl"] .dollar-symbol, +[dir="rtl"] .income-currency { + margin-right: 0; + margin-left: 8px; +} + +[dir="rtl"] .chevron { + margin-right: 8px; + margin-left: 0; + transform: rotate(180deg); +} + +[dir="rtl"] .cash-link .chevron { + margin-right: 5px; + margin-left: 0; +} + +[dir="rtl"] .policy-entry-left svg { + margin-right: 0; + margin-left: 18px; +} + +[dir="rtl"] .policy-entry-link > .chevron { + margin-right: 12px; + margin-left: 0; +} + +[dir="rtl"] .policy-region { + text-align: left; +} + +@media (max-width: 360px) { + .status-bar { + padding-right: 22px; + padding-left: 31px; + } + + .content { + padding-right: 12px; + padding-left: 12px; + } + + .profile-card, + .metrics-card, + .income-card { + padding-right: 18px; + padding-left: 18px; + } + + .profile-copy { + margin-left: 14px; + } + + .gift-metric { + padding-left: 18px; + } + + .income-value { + font-size: 29px; + } + + .policy-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + [dir="rtl"] .profile-copy { + margin-right: 14px; + margin-left: 0; + } +} diff --git a/h5/hyapp/withdraw-exchange/index.html b/h5/hyapp/withdraw-exchange/index.html new file mode 100644 index 0000000..e058921 --- /dev/null +++ b/h5/hyapp/withdraw-exchange/index.html @@ -0,0 +1,127 @@ + + + + + + Withdraw/Exchange + + + +
+ + + + + + +
+
+
Available Income
+
+ + 0.00 +
+
+ +
+
+ + +
+ +
+ +
+ Minimum 50 USD + +
+
+ +
+ +
+ You receive + $120.00 +
+
+
+ +
+
+
+
Payment method
+
The currently used bank card will receive this withdrawal.
+
+ +
+ +
+ +
+
No bank card in use
+
Set a bank card as in use before withdrawing.
+
+
+ + +
+ + +
+ + + +
+ + + diff --git a/h5/hyapp/withdraw-exchange/script.js b/h5/hyapp/withdraw-exchange/script.js new file mode 100644 index 0000000..80cbc5b --- /dev/null +++ b/h5/hyapp/withdraw-exchange/script.js @@ -0,0 +1,515 @@ +(function () { + const API_BASE_URL = "https://jvapi.haiyihy.com"; + const supportedLanguages = ["en", "ar", "tr", "id"]; + const languageLabels = { + en: "EN", + ar: "AR", + tr: "TR", + id: "ID" + }; + + const translations = { + en: { + page_label: "Withdraw and exchange", + back: "Back", + title: "Withdraw/Exchange", + language_button_aria: "Change language", + available_income: "Available Income", + withdraw: "Withdraw", + exchange: "Exchange", + withdraw_amount: "Withdraw amount", + exchange_amount: "Exchange amount", + minimum: "Minimum 50 USD", + all: "All", + you_receive: "You receive", + payment_method: "Payment method", + payment_method_subtitle: "The currently used bank card will receive this withdrawal.", + manage: "Manage", + no_bank_card: "No bank card in use", + no_bank_card_copy: "Set a bank card as in use before withdrawing.", + change: "Change", + withdraw_now: "Withdraw now", + exchange_now: "Exchange now", + submitting: "Submitting...", + payee: "Payee", + card_number: "Card", + missing_token: "Missing token", + load_balance_failed: "Failed to load balance", + load_bank_card_failed: "Failed to load bank card", + missing_bank_card: "No bank card in use", + invalid_amount: "Please enter a valid amount", + amount_too_low: "Minimum withdrawal amount is 50 USD", + amount_exceeds_balance: "Amount exceeds available income", + withdraw_success: "Withdrawal submitted", + withdraw_failed: "Withdrawal failed", + exchange_unavailable: "Exchange is not available on this page" + }, + ar: { + page_label: "السحب والتبديل", + back: "رجوع", + title: "سحب/تبديل", + language_button_aria: "تغيير اللغة", + available_income: "الدخل المتاح", + withdraw: "سحب", + exchange: "تبديل", + withdraw_amount: "مبلغ السحب", + exchange_amount: "مبلغ التبديل", + minimum: "الحد الأدنى 50 USD", + all: "الكل", + you_receive: "ستحصل على", + payment_method: "طريقة الدفع", + payment_method_subtitle: "سيتم إرسال السحب إلى البطاقة المستخدمة حاليًا.", + manage: "إدارة", + no_bank_card: "لا توجد بطاقة مستخدمة", + no_bank_card_copy: "عيّن بطاقة بنكية مستخدمة قبل السحب.", + change: "تغيير", + withdraw_now: "اسحب الآن", + exchange_now: "بدل الآن", + submitting: "جار الإرسال...", + payee: "المستفيد", + card_number: "البطاقة", + missing_token: "رمز الدخول مفقود", + load_balance_failed: "فشل تحميل الرصيد", + load_bank_card_failed: "فشل تحميل البطاقة", + missing_bank_card: "لا توجد بطاقة مستخدمة", + invalid_amount: "يرجى إدخال مبلغ صالح", + amount_too_low: "الحد الأدنى للسحب هو 50 USD", + amount_exceeds_balance: "المبلغ يتجاوز الدخل المتاح", + withdraw_success: "تم إرسال طلب السحب", + withdraw_failed: "فشل السحب", + exchange_unavailable: "التبديل غير متاح في هذه الصفحة" + }, + tr: { + page_label: "Çekme ve değişim", + back: "Geri", + title: "Çek/Değiştir", + language_button_aria: "Dili değiştir", + available_income: "Kullanılabilir Gelir", + withdraw: "Withdraw", + exchange: "Exchange", + withdraw_amount: "Withdraw amount", + exchange_amount: "Exchange amount", + minimum: "Minimum 50 USD", + all: "All", + you_receive: "You receive", + payment_method: "Payment method", + payment_method_subtitle: "The currently used bank card will receive this withdrawal.", + manage: "Manage", + no_bank_card: "No bank card in use", + no_bank_card_copy: "Set a bank card as in use before withdrawing.", + change: "Change", + withdraw_now: "Withdraw now", + exchange_now: "Exchange now", + submitting: "Submitting...", + payee: "Payee", + card_number: "Card", + missing_token: "Missing token", + load_balance_failed: "Failed to load balance", + load_bank_card_failed: "Failed to load bank card", + missing_bank_card: "No bank card in use", + invalid_amount: "Please enter a valid amount", + amount_too_low: "Minimum withdrawal amount is 50 USD", + amount_exceeds_balance: "Amount exceeds available income", + withdraw_success: "Withdrawal submitted", + withdraw_failed: "Withdrawal failed", + exchange_unavailable: "Exchange is not available on this page" + }, + id: { + page_label: "Tarik dan tukar", + back: "Kembali", + title: "Tarik/Tukar", + language_button_aria: "Ubah bahasa", + available_income: "Penghasilan Tersedia", + withdraw: "Tarik", + exchange: "Tukar", + withdraw_amount: "Jumlah penarikan", + exchange_amount: "Jumlah penukaran", + minimum: "Minimum 50 USD", + all: "Semua", + you_receive: "Anda menerima", + payment_method: "Metode pembayaran", + payment_method_subtitle: "Kartu bank yang sedang digunakan akan menerima penarikan ini.", + manage: "Kelola", + no_bank_card: "Tidak ada kartu aktif", + no_bank_card_copy: "Atur kartu bank sebagai aktif sebelum menarik.", + change: "Ubah", + withdraw_now: "Tarik sekarang", + exchange_now: "Tukar sekarang", + submitting: "Mengirim...", + payee: "Penerima", + card_number: "Kartu", + missing_token: "Token tidak ditemukan", + load_balance_failed: "Gagal memuat saldo", + load_bank_card_failed: "Gagal memuat kartu bank", + missing_bank_card: "Tidak ada kartu aktif", + invalid_amount: "Masukkan jumlah yang valid", + amount_too_low: "Minimum penarikan adalah 50 USD", + amount_exceeds_balance: "Jumlah melebihi penghasilan tersedia", + withdraw_success: "Penarikan terkirim", + withdraw_failed: "Penarikan gagal", + exchange_unavailable: "Penukaran tidak tersedia di halaman ini" + } + }; + + const params = readURLParams(); + let currentLang = normalizeLanguage(params.get("lang")); + let currentMode = "withdraw"; + let currentMessages = translations[currentLang] || translations.en; + let availableBalance = 0; + let selectedBankCard = null; + let isSubmitting = false; + + const amountInput = document.querySelector("#withdrawAmount"); + const balanceAmount = document.querySelector("#balanceAmount"); + const emptyState = document.querySelector(".empty-address-state"); + const boundCard = document.querySelector(".bound-address-card"); + const submitButton = document.querySelector(".submit-button"); + const toast = document.querySelector(".toast"); + + 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(); + return supportedLanguages.includes(value) ? value : "en"; + } + + function apiBaseURL() { + const override = window.HYAPP_API_BASE_URL || params.get("apiBase"); + if (override) return String(override).replace(/\/+$/, ""); + return API_BASE_URL; + } + + function normalizeAuthorization(token) { + const value = String(token || "").trim(); + if (!value) return ""; + return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`; + } + + function authHeadersFromToken(token, hasBody) { + const headers = { + 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" + }; + if (hasBody) headers["content-type"] = "application/json"; + return headers; + } + + async function requestJSON(path, options = {}) { + const token = readRawParam("token"); + if (!token) throw new Error(currentMessages.missing_token || translations.en.missing_token); + + const response = await fetch(`${apiBaseURL()}${path}`, { + method: options.method || "GET", + cache: "no-store", + headers: authHeadersFromToken(token, options.body !== undefined), + body: options.body === undefined ? undefined : JSON.stringify(options.body) + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || data.status === false) { + throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`); + } + return data; + } + + function message(key, fallback) { + return currentMessages[key] || translations.en[key] || fallback || key; + } + + function formatMoney(value) { + const number = Number(value); + return Number.isFinite(number) ? number.toFixed(2) : "0.00"; + } + + function moneyNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; + } + + function maskCardNumber(value) { + const text = String(value || "").trim(); + if (!text) return "-"; + if (text.length <= 8) return text; + return `${text.slice(0, 4)} **** ${text.slice(-4)}`; + } + + function activeAmount() { + return moneyNumber(amountInput?.value); + } + + function applyLanguage(lang) { + currentLang = normalizeLanguage(lang); + currentMessages = translations[currentLang] || translations.en; + + document.documentElement.lang = currentLang; + document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr"; + document.querySelectorAll("[data-i18n]").forEach((node) => { + const value = currentMessages[node.dataset.i18n]; + if (value) node.textContent = value; + }); + document.querySelectorAll("[data-i18n-aria]").forEach((node) => { + const value = currentMessages[node.dataset.i18nAria]; + if (value) node.setAttribute("aria-label", value); + }); + + const languageButton = document.querySelector(".language-button"); + if (languageButton) { + languageButton.textContent = languageLabels[currentLang]; + languageButton.setAttribute("aria-label", message("language_button_aria", "Change language")); + } + document.querySelectorAll(".language-menu [data-lang]").forEach((button) => { + const isActive = button.dataset.lang === currentLang; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); + updateSubmitState(); + } + + function setMode(mode) { + currentMode = mode; + document.querySelectorAll(".mode-tab").forEach((tab) => { + const active = tab.dataset.mode === mode; + tab.classList.toggle("active", active); + tab.setAttribute("aria-selected", String(active)); + }); + document.querySelectorAll(".mode-panel").forEach((panel) => { + panel.classList.toggle("active", panel.dataset.panel === mode); + }); + updateSubmitState(); + } + + function updateSubmitState() { + if (!submitButton) return; + if (isSubmitting) { + submitButton.textContent = message("submitting", "Submitting..."); + submitButton.disabled = true; + return; + } + + submitButton.textContent = currentMode === "exchange" ? message("exchange_now", "Exchange now") : message("withdraw_now", "Withdraw now"); + const amount = activeAmount(); + submitButton.disabled = currentMode !== "withdraw" || + !selectedBankCard?.id || + !amount || + amount < 50 || + amount > availableBalance; + } + + function showToast(keyOrText, fallback) { + if (!toast) return; + toast.textContent = message(keyOrText, fallback || keyOrText); + toast.hidden = false; + window.clearTimeout(showToast.timer); + showToast.timer = window.setTimeout(() => { + toast.hidden = true; + }, 2200); + } + + function renderBalance(value) { + availableBalance = moneyNumber(value); + if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance); + if (amountInput) amountInput.max = formatMoney(availableBalance); + updateSubmitState(); + } + + function renderBankCard(card) { + selectedBankCard = card || null; + + if (!selectedBankCard) { + if (emptyState) emptyState.hidden = false; + if (boundCard) boundCard.hidden = true; + updateSubmitState(); + return; + } + + const badge = boundCard?.querySelector(".network-badge"); + const network = boundCard?.querySelector(".bound-network"); + const address = boundCard?.querySelector(".bound-address"); + const meta = boundCard?.querySelector(".bound-meta"); + if (badge) badge.textContent = selectedBankCard.cardType || "BANK"; + if (network) network.textContent = selectedBankCard.cardType || "Bank card"; + if (address) address.textContent = `${message("card_number", "Card")}: ${maskCardNumber(selectedBankCard.cardNo)}`; + if (meta) meta.textContent = `${message("payee", "Payee")}: ${selectedBankCard.payee || "-"}`; + + if (emptyState) emptyState.hidden = true; + if (boundCard) boundCard.hidden = false; + updateSubmitState(); + } + + async function fetchBalance() { + try { + const data = await requestJSON("/wallet/bank/balance"); + const body = data.body ?? data.data ?? 0; + renderBalance(typeof body === "number" ? body : body.currentSalary ?? body.availableBalance ?? body.balance ?? 0); + } catch (error) { + console.error("Failed to load bank balance:", error); + renderBalance(0); + showToast("load_balance_failed", error.message); + } + } + + async function fetchBankCards() { + try { + const data = await requestJSON("/wallet/bank-card/list"); + const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : []; + renderBankCard(cards.find((item) => item.use)); + } catch (error) { + console.error("Failed to load bank card:", error); + renderBankCard(null); + showToast("load_bank_card_failed", error.message); + } + } + + function normalizeAmountInput() { + if (!amountInput) return; + const raw = String(amountInput.value || "").replace(/[^\d.]/g, ""); + const parts = raw.split("."); + let nextValue = parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : raw; + const amount = Number(nextValue); + if (Number.isFinite(amount) && amount > availableBalance) nextValue = formatMoney(availableBalance); + amountInput.value = nextValue; + updateSubmitState(); + } + + function validateWithdraw() { + const amount = activeAmount(); + if (!selectedBankCard?.id) return "missing_bank_card"; + if (!amount || amount <= 0) return "invalid_amount"; + if (amount < 50) return "amount_too_low"; + if (amount > availableBalance) return "amount_exceeds_balance"; + return ""; + } + + async function submitWithdraw() { + if (currentMode !== "withdraw") { + showToast("exchange_unavailable"); + return; + } + + const errorKey = validateWithdraw(); + if (errorKey) { + showToast(errorKey); + return; + } + + isSubmitting = true; + updateSubmitState(); + try { + await requestJSON("/wallet/bank/withdraw", { + method: "POST", + body: { + amount: activeAmount(), + acceptBankCardId: selectedBankCard.id, + salaryType: "HOST_SALARY" + } + }); + showToast("withdraw_success"); + await fetchBalance(); + } catch (error) { + console.error("Failed to submit withdrawal:", error); + showToast(error.message || message("withdraw_failed", "Withdrawal failed")); + } finally { + isSubmitting = false; + updateSubmitState(); + } + } + + function syncBackFallback() { + document.querySelector(".back-button")?.addEventListener("click", () => { + if (window.history.length > 1) { + window.history.back(); + return; + } + const nextParams = new URLSearchParams(params); + const token = readRawParam("token"); + if (token) nextParams.set("token", token); + if (currentLang) nextParams.set("lang", currentLang); + const query = nextParams.toString(); + window.location.href = `../host-center/index.html${query ? `?${query}` : ""}`; + }); + } + + function setLanguageMenuOpen(open) { + const languageButton = document.querySelector(".language-button"); + const languageMenu = document.querySelector(".language-menu"); + if (!languageButton || !languageMenu) return; + + languageMenu.hidden = !open; + languageButton.setAttribute("aria-expanded", String(open)); + } + + document.querySelector(".language-button")?.addEventListener("click", (event) => { + event.stopPropagation(); + const languageMenu = document.querySelector(".language-menu"); + setLanguageMenuOpen(Boolean(languageMenu?.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.querySelectorAll(".mode-tab").forEach((tab) => { + tab.addEventListener("click", () => setMode(tab.dataset.mode)); + }); + + document.querySelector(".text-action")?.addEventListener("click", () => { + if (amountInput) amountInput.value = formatMoney(availableBalance); + updateSubmitState(); + }); + + amountInput?.addEventListener("input", normalizeAmountInput); + submitButton?.addEventListener("click", submitWithdraw); + + syncBackFallback(); + applyLanguage(currentLang); + setMode("withdraw"); + fetchBalance(); + fetchBankCards(); +})(); diff --git a/h5/hyapp/withdraw-exchange/style.css b/h5/hyapp/withdraw-exchange/style.css new file mode 100644 index 0000000..8c3a3d7 --- /dev/null +++ b/h5/hyapp/withdraw-exchange/style.css @@ -0,0 +1,729 @@ +:root { + --page-bg: #f6f7f7; + --card-bg: rgba(255, 255, 255, 0.96); + --text: #25282e; + --muted: #8f9196; + --soft: #f1f3f3; + --line: #eef0f2; + --mint: #43e7d8; + --mint-dark: #16b9ac; + --gold: #e3b55b; + --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, +select { + font: inherit; +} + +button { + border: 0; + padding: 0; + background: transparent; + color: inherit; +} + +input, +select { + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: var(--text); +} + +.wallet-page { + position: relative; + width: 100%; + max-width: 430px; + min-height: 100vh; + overflow: hidden auto; + background: var(--page-bg); +} + +.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%); +} + +.status-bar, +.title-bar, +.content, +.home-indicator, +.toast { + position: relative; + z-index: 1; +} + +.status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 47px; + padding: 18px 29px 0 38px; + color: #11161c; + font-size: 15px; + font-weight: 800; +} + +.status-icons { + display: flex; + align-items: center; + gap: 6px; +} + +.cellular { + display: flex; + align-items: flex-end; + gap: 2px; + width: 18px; + height: 12px; +} + +.cellular i { + width: 3px; + border-radius: 2px; + background: #11161c; +} + +.cellular i:nth-child(1) { + height: 4px; +} + +.cellular i:nth-child(2) { + height: 6px; +} + +.cellular i:nth-child(3) { + height: 9px; +} + +.cellular i:nth-child(4) { + height: 11px; +} + +.wifi { + position: relative; + width: 18px; + height: 13px; +} + +.wifi i { + position: absolute; + left: 50%; + border: 2px solid #11161c; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; + border-radius: 12px 12px 0 0; + transform: translateX(-50%); +} + +.wifi i:nth-child(1) { + top: 1px; + width: 16px; + height: 8px; +} + +.wifi i:nth-child(2) { + top: 5px; + width: 10px; + height: 5px; +} + +.wifi i:nth-child(3) { + top: 10px; + width: 4px; + height: 4px; + border: 0; + border-radius: 50%; + background: #11161c; +} + +.battery { + position: relative; + width: 24px; + height: 12px; + border: 2px solid #11161c; + border-radius: 3px; +} + +.battery::after { + content: ""; + position: absolute; + top: 3px; + right: -4px; + width: 2px; + height: 4px; + border-radius: 0 2px 2px 0; + background: #11161c; +} + +.battery i { + display: block; + width: 16px; + height: 100%; + background: #11161c; +} + +.title-bar { + display: grid; + grid-template-columns: 52px 1fr 52px; + align-items: center; + height: 60px; + padding: 0 14px; + direction: ltr; +} + +.title-bar h1 { + margin: 0; + color: #24282e; + font-size: 21px; + font-weight: 900; + line-height: 1; + text-align: center; +} + +.back-button { + display: flex; + align-items: center; + justify-content: flex-start; + width: 44px; + height: 44px; +} + +.back-button svg { + width: 28px; + height: 28px; +} + +.back-button path { + fill: none; + stroke: #24282e; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 3; +} + +.language-switcher { + position: relative; + justify-self: end; +} + +.language-button { + justify-self: end; + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 30px; + border: 1px solid rgba(35, 40, 46, 0.12); + border-radius: 15px; + background: rgba(255, 255, 255, 0.68); + color: #25282e; + font-size: 13px; + font-weight: 900; + box-shadow: 0 4px 12px rgba(43, 118, 107, 0.08); +} + +.language-menu { + position: absolute; + top: calc(100% + 7px); + right: 0; + display: grid; + gap: 2px; + min-width: 58px; + padding: 5px; + border: 1px solid rgba(35, 40, 46, 0.1); + border-radius: 8px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 10px 26px rgba(36, 46, 60, 0.14); +} + +.language-menu button { + min-height: 28px; + border-radius: 6px; + color: #25282e; + font-size: 12px; + font-weight: 900; +} + +.language-menu button.is-active { + background: #e6fbf8; + color: #0e8f82; +} + +.language-menu[hidden] { + display: none; +} + +.content { + display: flex; + flex-direction: column; + gap: 14px; + padding: 8px 14px 92px; +} + +.balance-card, +.mode-card, +.card-section, +.bind-panel { + width: 100%; + border-radius: 8px; + background: var(--card-bg); + box-shadow: var(--shadow); +} + +.balance-card { + padding: 20px 24px 19px; +} + +.balance-label { + color: var(--muted); + font-size: 14px; + font-weight: 800; +} + +.balance-value { + display: flex; + align-items: center; + margin-top: 10px; + color: #23272d; + font-size: 34px; + font-weight: 950; + line-height: 1.05; +} + +.balance-currency { + flex: 0 0 auto; + margin-right: 6px; + color: var(--gold); + font-size: 28px; + font-weight: 950; + line-height: 1; +} + +.mode-card { + padding: 16px; +} + +.mode-tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + padding: 4px; + border-radius: 8px; + background: #eef1f1; +} + +.mode-tab { + min-height: 40px; + border-radius: 7px; + color: #777a80; + font-size: 15px; + font-weight: 900; +} + +.mode-tab.active { + background: #fff; + color: var(--text); + box-shadow: 0 5px 14px rgba(38, 51, 62, 0.08); +} + +.mode-panel { + display: none; + margin-top: 16px; +} + +.mode-panel.active { + display: block; +} + +.field { + display: block; +} + +.field > span { + display: block; + margin-bottom: 8px; + color: #4a4d53; + font-size: 14px; + font-weight: 850; +} + +.field-control, +.field input, +.field select { + min-height: 48px; + border-radius: 8px; + background: #f4f6f6; +} + +.field input, +.field select { + padding: 0 14px; +} + +.amount-control { + display: flex; + align-items: center; + padding: 0 14px; +} + +.amount-control span { + flex: 0 0 auto; + margin-right: 9px; + color: var(--gold); + font-size: 21px; + font-weight: 950; +} + +.amount-control input { + min-height: 0; + padding: 0; + background: transparent; + font-size: 23px; + font-weight: 900; +} + +.hint-row, +.exchange-preview { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; + color: var(--muted); + font-size: 13px; + font-weight: 800; +} + +.text-action { + color: var(--mint-dark); + font-weight: 950; +} + +.exchange-preview strong { + color: #24282e; +} + +.card-section, +.bind-panel { + padding: 18px 16px; +} + +.section-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.section-title { + color: #24282e; + font-size: 17px; + font-weight: 900; +} + +.section-subtitle { + margin-top: 4px; + color: var(--muted); + font-size: 13px; + font-weight: 750; +} + +.bind-address-button, +.change-address { + flex: 0 0 auto; + min-height: 34px; + padding: 0 13px; + border-radius: 17px; + background: rgba(67, 231, 216, 0.14); + color: var(--mint-dark); + font-size: 13px; + font-weight: 950; +} + +.empty-address-state { + display: flex; + align-items: center; + gap: 13px; + min-height: 76px; + margin-top: 16px; + padding: 13px; + border: 1px dashed #d9dddd; + border-radius: 8px; + background: #fbfcfc; +} + +.empty-icon { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + flex: 0 0 42px; + border-radius: 50%; + background: #edf8f7; +} + +.empty-icon svg { + width: 24px; + height: 24px; +} + +.empty-icon path { + fill: none; + stroke: var(--mint-dark); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.2; +} + +.empty-title { + color: #292d33; + font-size: 15px; + font-weight: 900; +} + +.empty-copy { + margin-top: 3px; + color: var(--muted); + font-size: 12px; + font-weight: 750; + line-height: 1.25; +} + +.bound-address-card { + display: flex; + align-items: center; + gap: 14px; + min-height: 88px; + margin-top: 16px; + padding: 14px; + border-radius: 8px; + background: linear-gradient(135deg, #1e252d 0%, #354858 100%); + color: #fff; +} + +.network-badge { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 32px; + flex: 0 0 56px; + border-radius: 7px; + background: linear-gradient(135deg, #ffe58b 0%, #c8942e 100%); + color: #1f252c; + font-size: 12px; + font-weight: 950; +} + +.bound-copy { + flex: 1 1 auto; + min-width: 0; +} + +.bound-network { + font-size: 15px; + font-weight: 900; +} + +.bound-address { + margin-top: 5px; + color: rgba(255, 255, 255, 0.82); + font-size: 14px; + font-weight: 850; + letter-spacing: 0; +} + +.bound-meta { + margin-top: 4px; + color: rgba(255, 255, 255, 0.62); + font-size: 12px; + font-weight: 750; +} + +.change-address { + background: rgba(255, 255, 255, 0.18); + color: #fff; +} + +.bind-panel { + display: block; +} + +.address-form { + display: flex; + flex-direction: column; + gap: 13px; + margin-top: 14px; +} + +.network-field { + display: flex; + align-items: center; + gap: 9px; + min-height: 48px; + padding: 0 14px; + border-radius: 8px; + background: #f4f6f6; +} + +.network-field strong { + color: #25282e; + font-size: 15px; + font-weight: 950; +} + +.network-field span { + padding: 4px 8px; + border-radius: 999px; + background: rgba(67, 231, 216, 0.16); + color: var(--mint-dark); + font-size: 12px; + font-weight: 950; +} + +.network-note { + margin-top: -2px; + color: var(--muted); + font-size: 12px; + font-weight: 750; + line-height: 1.35; +} + +.form-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 2px; +} + +.secondary-button, +.primary-button, +.submit-button { + min-height: 48px; + border-radius: 24px; + font-size: 16px; + font-weight: 950; +} + +.secondary-button { + background: #eef1f1; + color: #55585f; +} + +.primary-button, +.submit-button { + background: linear-gradient(135deg, #67f4e8 0%, #27d3c4 100%); + color: #11302d; +} + +.submit-button { + width: 100%; + box-shadow: 0 8px 20px rgba(29, 206, 193, 0.22); +} + +.submit-button:disabled { + background: #d7dada; + color: #8b8e94; + box-shadow: none; +} + +.toast { + position: fixed; + left: 50%; + bottom: 34px; + transform: translateX(-50%); + padding: 10px 14px; + border-radius: 999px; + background: rgba(24, 29, 34, 0.92); + color: #fff; + font-size: 13px; + font-weight: 850; +} + +.home-indicator { + position: fixed; + left: 50%; + bottom: 8px; + width: 154px; + height: 5px; + border-radius: 999px; + background: #050506; + transform: translateX(-50%); +} + +[hidden] { + display: none !important; +} + +[dir="rtl"] .content { + direction: rtl; +} + +[dir="rtl"] .balance-currency, +[dir="rtl"] .amount-control span { + margin-right: 0; + margin-left: 9px; +} + +[dir="rtl"] .toast, +[dir="rtl"] .home-indicator { + direction: ltr; +} + +@media (max-width: 360px) { + .status-bar { + padding-right: 22px; + padding-left: 31px; + } + + .content { + padding-right: 12px; + padding-left: 12px; + } + + .balance-card { + padding-right: 18px; + padding-left: 18px; + } + + .balance-value { + font-size: 29px; + } + + .title-bar h1 { + font-size: 19px; + } +}