(function () { 'use strict'; var ASSET_BASE = './huwaa/assets/layers/'; var params = new URLSearchParams(window.location.search || ''); var useMock = params.get('mock') === '1'; var DESIGN_PRIZES = [ { id: 'medium-orange', name: 'Medium', image: ASSET_BASE + 'chest-orange.webp', points: 500, }, { id: 'medium-green', name: 'Medium', image: ASSET_BASE + 'chest-green.webp', points: 500, }, { id: 'medium-purple', name: 'Medium', image: ASSET_BASE + 'chest-white-gold.webp', points: 500, }, { id: 'medium-teal', name: 'Medium', image: ASSET_BASE + 'chest-blue.webp', points: 500, }, { id: 'medium-gold', name: 'Medium', image: ASSET_BASE + 'chest-purple.webp', points: 500, }, { id: 'medium-red', name: 'Medium', image: ASSET_BASE + 'chest-red.webp', points: 500, }, { id: 'medium-brown', name: 'Medium', image: ASSET_BASE + 'chest-gold.webp', points: 500, }, ]; var DESIGN_TASKS = [ { id: 'microphone-1', title: 'Use the microphone for 1 min', description: 'Friend completes biometric authentication', titleKey: 'huwaa_invite.task_title', descriptionKey: 'huwaa_invite.task_description', reward: 1, completed: true, }, { id: 'microphone-2', title: 'Use the microphone for 1 min', description: 'Friend completes biometric authentication', titleKey: 'huwaa_invite.task_title', descriptionKey: 'huwaa_invite.task_description', reward: 1, completed: true, }, { id: 'microphone-3', title: 'Use the microphone for 1 min', description: 'Friend completes biometric authentication', titleKey: 'huwaa_invite.task_title', descriptionKey: 'huwaa_invite.task_description', reward: 1, completed: true, }, ]; var state = { wheelID: params.get('wheel_id') || params.get('wheelId') || 'cash', busy: false, pointBalance: useMock ? 89094 : 0, cashValue: useMock ? 8.9 : 0, ratePoints: '90K', withdrawalRemaining: useMock ? 1000 : 0, drawCost: 5, prizes: DESIGN_PRIZES.slice(), tasks: DESIGN_TASKS.slice(), estimatedRewards: [], inviteCode: '', inviteLink: '', spinCount: 0, }; function $(selector) { return document.querySelector(selector); } function $all(selector) { return Array.prototype.slice.call(document.querySelectorAll(selector)); } function t(key, fallback) { if (window.HyAppI18n && window.HyAppI18n.t) { return window.HyAppI18n.t(key, fallback); } return fallback || key; } function toast(message) { var target = window.HyToast || window.HyAppToast; if (target && target.show) { target.show(message); return; } window.alert(message); } function text(value) { return String( value === undefined || value === null ? '' : value ).trim(); } function number(value, fallback) { var result = Number(value); return Number.isFinite(result) ? result : Number(fallback || 0); } function first(source, keys) { source = source || {}; for (var index = 0; index < keys.length; index += 1) { var value = source[keys[index]]; if (value !== undefined && value !== null && value !== '') { return value; } } return ''; } function envelope(payload) { if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) { return payload.data; } return payload || {}; } function parseJSON(value) { if (!value || typeof value !== 'string') return {}; try { return JSON.parse(value); } catch (error) { return {}; } } function formatNumber(value) { var lang = window.HyAppI18n && window.HyAppI18n.lang ? window.HyAppI18n.lang() : 'en'; return new Intl.NumberFormat(lang).format(number(value)); } function resolveAssetURL(value) { value = text(value); if (!value) return ''; if ( /^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value) || value.charAt(0) === '/' || value.indexOf('./') === 0 || value.indexOf('../') === 0 ) { return value; } // 旧版资源组只返回 wheel/assets/other 下的裸文件名;统一补齐路径,避免把配置内容误当当前 invite 目录的相对文件。 return new URL('../wheel/assets/other/' + value, window.location.href) .href; } function normalizeReward(item, fallback, index) { item = item || {}; fallback = fallback || DESIGN_PRIZES[index % DESIGN_PRIZES.length]; var metadata = parseJSON( first(item, ['metadata_json', 'metadataJson']) ); var image = first(metadata, [ 'preview_url', 'previewUrl', 'asset_url', 'assetUrl', ]) || first(item, [ 'cover_url', 'coverUrl', 'cover', 'resource_cover', 'resourceCover', 'preview_url', 'previewUrl', 'image_url', 'imageUrl', 'icon', 'avatar', ]); var name = first(item, [ 'display_name', 'displayName', 'reward_name', 'rewardName', 'name', ]) || fallback.name; var points = number( first(item, [ 'display_value', 'displayValue', 'reward_value', 'rewardValue', 'reward_coins', 'rewardCoins', 'points', 'value', 'amount', ]), fallback.points ); return { id: text( first(item, [ 'id', 'tier_id', 'tierId', 'reward_id', 'rewardId', 'resource_id', 'resourceId', ]) ) || fallback.id, name: text(name) || fallback.name, image: resolveAssetURL(image) || fallback.image, points: points, raw: item, }; } function rewardItems(payload) { var data = envelope(payload); var values = Array.isArray(data) ? data : data.rewards || data.tiers || data.items || (data.config && (data.config.rewards || data.config.tiers)) || []; if (!Array.isArray(values)) return []; return values.map(function (item, index) { return normalizeReward(item, DESIGN_PRIZES[index], index); }); } function normalizeTasks(payload) { var data = envelope(payload); var values = data.tasks || data.invite_tasks || data.inviteTasks || (data.config && data.config.tasks) || []; if (!Array.isArray(values)) return []; return values.slice(0, 3).map(function (item, index) { var status = text(first(item, ['status', 'state'])).toLowerCase(); return { id: text(first(item, ['id', 'task_id', 'taskId'])) || 'task-' + index, title: text(first(item, ['title', 'name'])) || DESIGN_TASKS[index].title, description: text( first(item, ['description', 'desc', 'subtitle', 'hint']) ) || DESIGN_TASKS[index].description, reward: number( first(item, [ 'reward_spins', 'rewardSpins', 'reward', 'value', ]), 1 ), completed: first(item, [ 'completed', 'is_completed', 'isCompleted', ]) === true || status === 'completed' || status === 'claimed', action: first(item, ['action', 'action_url', 'actionUrl']), raw: item, }; }); } function normalizeWalletBalance(payload) { var data = envelope(payload); var values = Array.isArray(data) ? data : data.balances || data.values || data.list || []; if (!Array.isArray(values)) return 0; var wallet = values.find(function (item) { return ( text( first(item, [ 'asset_type', 'assetType', 'wallet_type', 'walletType', 'type', ]) ).toUpperCase() === 'COIN' ); }); if (!wallet) return 0; return number( first(wallet, [ 'available_amount', 'availableAmount', 'balance', 'amount', ]) ); } function createCommandID() { var random = Math.random().toString(36).slice(2); if (window.crypto && window.crypto.getRandomValues) { var values = new Uint32Array(2); window.crypto.getRandomValues(values); random = values[0].toString(36) + values[1].toString(36); } return 'huwaa_cash_' + Date.now().toString(36) + '_' + random; } function setText(selector, value) { var node = $(selector); if (node) node.textContent = value; } function renderWallet() { setText('#huwaaRatePoints', state.ratePoints); setText('#huwaaPointBalance', formatNumber(state.pointBalance)); setText( '#huwaaCashValue', '≈$' + number(state.cashValue, state.pointBalance / 10000).toFixed(1) ); setText('#huwaaPointsLeft', formatNumber(state.withdrawalRemaining)); setText('#huwaaSpinCost', '*' + formatNumber(state.drawCost)); } function renderPrizes() { $all('.huwaa-prize').forEach(function (button, index) { var prize = state.prizes[index] || DESIGN_PRIZES[index]; var image = button.querySelector('img'); var label = button.querySelector('span'); image.src = prize.image; image.onerror = function () { image.onerror = null; image.src = DESIGN_PRIZES[index].image; }; var prizeName = prize.raw ? prize.name : t('huwaa_invite.medium', 'Medium'); label.textContent = prizeName; button.setAttribute( 'aria-label', prizeName + (prize.points ? ', ' + formatNumber(prize.points) + ' points' : '') ); }); } function renderTasks() { var list = $('#huwaaTaskList'); var template = $('#huwaaTaskTemplate'); if (!list || !template) return; list.textContent = ''; state.tasks.slice(0, 3).forEach(function (task) { var fragment = template.content.cloneNode(true); var card = fragment.querySelector('.huwaa-task-card'); var status = fragment.querySelector('[data-task-status]'); fragment.querySelector('[data-task-title]').textContent = task.titleKey ? t(task.titleKey, task.title) : task.title; fragment.querySelector('[data-task-description]').textContent = task.descriptionKey ? t(task.descriptionKey, task.description) : task.description; fragment.querySelector('[data-task-reward]').textContent = '+' + formatNumber(task.reward); card.classList.toggle('is-pending', !task.completed); if (task.completed) { status.textContent = t('huwaa_invite.completed', 'COMPLETED'); } else { var action = document.createElement('button'); action.className = status.className; action.type = 'button'; action.textContent = t('huwaa_invite.go', 'GO'); action.addEventListener('click', function () { openTask(task); }); status.replaceWith(action); } list.appendChild(fragment); }); } function defaultEstimatedRewards() { var values = []; for (var index = 0; index < 12; index += 1) { values.push({ id: 'estimated-' + index, name: useMock ? 'DDDDD' : t('huwaa_invite.reward', 'Reward'), image: ASSET_BASE + 'reward-calendar-check.webp', points: 0, }); } return values; } function renderEstimatedRewards() { var grid = $('#huwaaEstimatedGrid'); var template = $('#huwaaEstimatedTemplate'); if (!grid || !template) return; var rewards = state.estimatedRewards.length ? state.estimatedRewards : defaultEstimatedRewards(); grid.textContent = ''; rewards.slice(0, 12).forEach(function (reward) { var fragment = template.content.cloneNode(true); var image = fragment.querySelector('img'); image.src = reward.image || ASSET_BASE + 'reward-calendar-check.webp'; image.onerror = function () { image.onerror = null; image.src = ASSET_BASE + 'reward-calendar-check.webp'; }; fragment.querySelector('strong').textContent = reward.name || t('huwaa_invite.reward', 'Reward'); grid.appendChild(fragment); }); } function renderAll() { renderWallet(); renderPrizes(); renderTasks(); renderEstimatedRewards(); } function setBusy(busy) { state.busy = Boolean(busy); $('#huwaaStage').classList.toggle('is-busy', state.busy); $('#huwaaSpinButton').disabled = state.busy; $('#huwaaStage').setAttribute('aria-busy', String(state.busy)); } function setActivePrize(index) { $all('.huwaa-prize').forEach(function (node, nodeIndex) { node.classList.toggle('is-active', nodeIndex === index); }); } function wait(milliseconds) { return new Promise(function (resolve) { window.setTimeout(resolve, milliseconds); }); } function animateSpin(targetIndex) { var total = state.prizes.length || DESIGN_PRIZES.length; var loops = total * 2 + targetIndex + 1; var chain = Promise.resolve(); for (var step = 0; step < loops; step += 1) { (function (currentStep) { chain = chain.then(function () { setActivePrize(currentStep % total); return wait(60 + Math.max(0, currentStep - loops + 6) * 25); }); })(step); } return chain.then(function () { setActivePrize(targetIndex); }); } function rewardTargetIndex(reward) { if (!reward) return 0; var id = text(reward.id).toLowerCase(); var name = text(reward.name).toLowerCase(); var points = number(reward.points); var index = state.prizes.findIndex(function (prize) { return id && text(prize.id).toLowerCase() === id; }); if (index >= 0) return index; index = state.prizes.findIndex(function (prize) { return ( name && text(prize.name).toLowerCase() === name && (!points || number(prize.points) === points) ); }); return index >= 0 ? index : 0; } function normalizeDrawResult(payload) { var data = envelope(payload); var rawRewards = data.rewards || data.records || data.items || data.reward_items || []; var firstReward = Array.isArray(rawRewards) ? rawRewards[0] : rawRewards; var normalized = normalizeReward( firstReward || data, state.prizes[0], 0 ); var balanceAfter = first(data, [ 'coin_balance_after', 'coinBalanceAfter', 'balance_after', 'balanceAfter', ]); return { reward: normalized, balanceAfter: balanceAfter === '' ? undefined : number(balanceAfter), }; } function mockDrawResult() { var index = state.spinCount % state.prizes.length; state.spinCount += 1; return Promise.resolve({ reward: Object.assign({}, state.prizes[index], { name: 'Medium Reward', points: 500, }), // 视觉夹具保持 Figma 三个状态使用同一余额;真实环境只接受服务端返回的扣减后余额。 balanceAfter: undefined, }); } function requestDraw() { if (useMock) return mockDrawResult(); var api = window.HyAppAPI && window.HyAppAPI.activityWheel; var hasToken = window.HyAppAPI && window.HyAppAPI.getAccessToken && window.HyAppAPI.getAccessToken(); if (!api || !api.draw || !hasToken) { return Promise.reject( new Error( t('huwaa_invite.login_required', 'Please log in first') ) ); } // 抽奖金额和 command_id 必须一次性发给服务端;页面不在失败时本地补发奖励,避免网络错误造成视觉中奖但账本未入账。 return api .draw(state.wheelID, { command_id: createCommandID(), amount: state.drawCost, times: 1, }) .then(normalizeDrawResult); } function showWin(reward) { reward = reward || state.prizes[0]; var image = $('#huwaaWinImage'); image.src = reward.image || state.prizes[0].image; image.onerror = function () { image.onerror = null; image.src = state.prizes[0].image; }; setText( '#huwaaWinReward', reward.raw ? reward.name : t('huwaa_invite.medium_reward', 'Medium Reward') ); setText( '#huwaaWinPoints', '(' + formatNumber(reward.points || 0) + ' ' + t('huwaa_invite.points_label', 'Points') + ')' ); $('#huwaaWinModal').hidden = false; syncModalLock(); $('#huwaaWinTitle').focus({ preventScroll: true }); } function closeWin() { $('#huwaaWinModal').hidden = true; syncModalLock(); setActivePrize(-1); $('#huwaaSpinButton').focus({ preventScroll: true }); } function startSpin() { if (state.busy) return; setBusy(true); requestDraw() .then(function (result) { var target = rewardTargetIndex(result.reward); return animateSpin(target).then(function () { if (result.balanceAfter !== undefined) { state.pointBalance = result.balanceAfter; state.cashValue = state.pointBalance / 10000; renderWallet(); } showWin(result.reward); if (!useMock) loadEstimatedRewards(); }); }) .catch(function (error) { setActivePrize(-1); toast( (error && error.message) || t('huwaa_invite.request_failed', 'Request failed') ); }) .then(function () { setBusy(false); }); } function openRewardDrawer() { renderEstimatedRewards(); $('#huwaaRewardDrawer').hidden = false; syncModalLock(); $('#huwaaRewardTitle').focus ? $('#huwaaRewardTitle').focus({ preventScroll: true }) : undefined; } function closeRewardDrawer() { $('#huwaaRewardDrawer').hidden = true; syncModalLock(); $('#huwaaRewardEntry').focus({ preventScroll: true }); } function syncModalLock() { document.documentElement.classList.toggle( 'huwaa-modal-open', !$('#huwaaWinModal').hidden || !$('#huwaaRewardDrawer').hidden ); } function openTask(task) { if (task.completed) return; if (task.action) { window.location.href = task.action; return; } if (window.HyAppBridge && window.HyAppBridge.post) { window.HyAppBridge.post('huwaaInviteTaskAction', { task_id: task.id, }); return; } toast(t('huwaa_invite.open_app_task', 'Open this task in Huwaa')); } function currentInviteLandingURL() { return new URL('../invite-landing/', window.location.href); } function buildInviteLink() { if (!state.inviteCode) return ''; var url = currentInviteLandingURL(); ['env', 'lang', 'app_code', 'appCode'].forEach(function (key) { var value = text(params.get(key)); if (value) url.searchParams.set(key, value); }); url.searchParams.set('app_code', 'huwaa'); url.searchParams.set('invite_code', state.inviteCode); url.searchParams.set('inviteCode', state.inviteCode); url.searchParams.set('code', state.inviteCode); return url.toString(); } function copyText(value) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(value); } return new Promise(function (resolve, reject) { var input = document.createElement('textarea'); input.value = value; input.setAttribute('readonly', 'readonly'); input.style.position = 'fixed'; input.style.left = '-9999px'; document.body.appendChild(input); input.select(); var copied = false; try { copied = document.execCommand('copy'); } catch (error) { copied = false; } input.remove(); copied ? resolve() : reject(new Error('copy failed')); }); } function shareInvite() { var link = state.inviteLink || buildInviteLink(); if (!link) { toast( t( 'huwaa_invite.invite_unavailable', 'Invitation link is not available yet' ) ); return; } if (navigator.share) { navigator .share({ title: t('huwaa_invite.invite_now', 'Invite Friends Now'), url: link, }) .catch(function (error) { if (error && error.name === 'AbortError') return; copyInviteLink(link); }); return; } copyInviteLink(link); } function copyInviteLink(link) { copyText(link) .then(function () { toast(t('huwaa_invite.copied', 'Copied')); }) .catch(function () { toast(t('huwaa_invite.copy_failed', 'Copy failed')); }); } function withdraw() { var walletURL = text( params.get('wallet_url') || params.get('walletUrl') ); if (walletURL) { window.location.href = walletURL; return; } if (window.HyAppBridge && window.HyAppBridge.post) { window.HyAppBridge.post('huwaaCashWheelWithdraw', { points: state.pointBalance, }); return; } toast( t('huwaa_invite.open_wallet', 'Open the Huwaa wallet to withdraw') ); } function goBack() { if (window.HyAppBridge && window.HyAppBridge.back) { window.HyAppBridge.back(); return; } if (window.history.length > 1) { window.history.back(); } } function applyWheelConfig(payload) { var data = envelope(payload); var prizes = rewardItems(data); var tasks = normalizeTasks(data); var drawCost = number( first(data, [ 'draw_price_coins', 'drawPriceCoins', 'draw_price', 'drawPrice', 'spin_cost', 'spinCost', ]) ); if (prizes.length) { state.prizes = prizes.slice(0, 7); while (state.prizes.length < 7) { state.prizes.push(DESIGN_PRIZES[state.prizes.length]); } if (!state.estimatedRewards.length) { state.estimatedRewards = prizes.slice(0, 12); } } if (tasks.length) state.tasks = tasks; if (drawCost > 0) state.drawCost = drawCost; var threshold = number( first(data, [ 'withdraw_threshold', 'withdrawThreshold', 'withdraw_points', 'withdrawPoints', ]) ); if (threshold > 0) { state.ratePoints = threshold >= 1000 ? Math.round(threshold / 1000) + 'K' : formatNumber(threshold); state.withdrawalRemaining = Math.max( 0, threshold - state.pointBalance ); } renderAll(); } function loadWheelConfig() { var api = window.HyAppAPI && window.HyAppAPI.activityWheel; if (useMock || !api || !api.config) return Promise.resolve(); return api .config(state.wheelID) .then(applyWheelConfig) .catch(function () { // 配置缺失时保留 Figma 的独立本地视觉层,但抽奖仍走服务端且会失败提示,不在前端伪造奖励。 }); } function loadWallet() { var api = window.HyAppAPI && window.HyAppAPI.wallet; var hasToken = window.HyAppAPI && window.HyAppAPI.getAccessToken && window.HyAppAPI.getAccessToken(); if (useMock || !api || !api.balances || !hasToken) { return Promise.resolve(); } return api .balances('COIN') .then(function (payload) { state.pointBalance = normalizeWalletBalance(payload); state.cashValue = state.pointBalance / 10000; renderWallet(); }) .catch(function () { // 钱包失败不能阻塞页面结构;余额保持 0,实际抽奖仍由服务端余额校验兜底。 }); } function normalizeHistoryRewards(payload) { var data = envelope(payload); var records = data.records || data.items || data.values || []; if (!Array.isArray(records)) return []; var rewards = []; records.forEach(function (record) { var values = record.rewards || record.items || record.reward_items || [record]; if (!Array.isArray(values)) values = [values]; values.forEach(function (value) { rewards.push( normalizeReward( value, DESIGN_PRIZES[rewards.length % DESIGN_PRIZES.length], rewards.length ) ); }); }); return rewards.slice(0, 12); } function loadEstimatedRewards() { var api = window.HyAppAPI && window.HyAppAPI.activityWheel; var hasToken = window.HyAppAPI && window.HyAppAPI.getAccessToken && window.HyAppAPI.getAccessToken(); if (useMock || !api || !api.history || !hasToken) { renderEstimatedRewards(); return Promise.resolve(); } return api .history(state.wheelID, { pageNo: 1, pageSize: 12 }) .then(function (payload) { var rewards = normalizeHistoryRewards(payload); if (rewards.length) state.estimatedRewards = rewards; renderEstimatedRewards(); }) .catch(function () { renderEstimatedRewards(); }); } function applyInviteOverview(payload) { var data = envelope(payload); var invite = data.invite || (data.profile && data.profile.invite) || {}; state.inviteCode = text( first(invite, [ 'my_invite_code', 'myInviteCode', 'invite_code', 'inviteCode', ]) || first(data, [ 'my_invite_code', 'myInviteCode', 'invite_code', 'inviteCode', ]) ); state.inviteLink = buildInviteLink(); } function loadInviteOverview() { if (useMock) { state.inviteCode = 'HUWAA2026'; state.inviteLink = buildInviteLink(); return Promise.resolve(); } var api = window.HyAppAPI; var hasToken = api && api.getAccessToken && api.getAccessToken(); if (!api || !api.get || !hasToken) return Promise.resolve(); return api .get('/api/v1/users/me/invite-overview') .then(applyInviteOverview) .catch(function () { // 分享按钮在邀请数据缺失时保持不可分享状态,绝不拼接伪邀请码。 }); } function bindEvents() { $('#huwaaBackButton').addEventListener('click', goBack); $('#huwaaWithdrawButton').addEventListener('click', withdraw); $('#huwaaSpinButton').addEventListener('click', startSpin); $('#huwaaInviteButton').addEventListener('click', shareInvite); $('#huwaaRewardEntry').addEventListener('click', openRewardDrawer); $('#huwaaAwesomeButton').addEventListener('click', closeWin); $('[data-close-win]').addEventListener('click', closeWin); $('[data-close-rewards]').addEventListener('click', closeRewardDrawer); $all('.huwaa-prize').forEach(function (button) { button.addEventListener('click', openRewardDrawer); }); document.addEventListener('keydown', function (event) { if (event.key !== 'Escape') return; if (!$('#huwaaWinModal').hidden) closeWin(); if (!$('#huwaaRewardDrawer').hidden) closeRewardDrawer(); }); } function init() { bindEvents(); renderAll(); $('#huwaaPage').dataset.state = 'loading'; Promise.all([ loadWallet(), loadWheelConfig(), loadEstimatedRewards(), loadInviteOverview(), ]).then(function () { $('#huwaaPage').dataset.state = 'ready'; }); if (window.HyAppBridge && window.HyAppBridge.ready) { window.HyAppBridge.ready({ page: 'huwaa-invite-cash-wheel', app_code: 'huwaa', }); } } window.addEventListener('hyapp:i18n-ready', function () { renderAll(); }); window.__HuwaaInvite = { state: state, openRewards: openRewardDrawer, spin: startSpin, }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();