feat: add mobile banking app and integrate into phone UI
- Implemented wallet app functionality including account management, transaction handling, and user notifications. - Updated HomeScreen to include Wallet app icon. - Enhanced StateManager to maintain mobile bank state. - Created wallet-specific styles for UI consistency. - Updated CSS and JS concatenation scripts to include wallet resources.
This commit is contained in:
parent
008631ed10
commit
5c76d59baf
@ -3,6 +3,26 @@
|
||||
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
GVAR(sendPhoneBankEvent) = {
|
||||
params [["_functionName", "", [""]], ["_arguments", [], [[]]]];
|
||||
|
||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||
if (isNull _display || { _functionName isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _control = _display displayCtrl 1001;
|
||||
if (isNull _control) exitWith { false };
|
||||
|
||||
private _serializedArguments = _arguments apply { toJSON _x };
|
||||
private _script = format [
|
||||
"window.%1 && window.%1(%2)",
|
||||
_functionName,
|
||||
_serializedArguments joinString ", "
|
||||
];
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", _script];
|
||||
true
|
||||
};
|
||||
|
||||
[QGVAR(initBank), {
|
||||
GVAR(BankRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
@ -14,6 +34,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncBank), {
|
||||
@ -23,6 +44,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateBank), {
|
||||
@ -31,6 +53,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
||||
};
|
||||
["updateMobileBank", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseBankNotice), {
|
||||
@ -39,6 +62,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
||||
};
|
||||
["showMobileBankNotice", [_type, _message]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
|
||||
@ -345,6 +345,56 @@ switch (_event) do {
|
||||
|
||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
||||
};
|
||||
case "phone::bank::refresh": {
|
||||
["forge_server_bank_requestHydrateBank", [getPlayerUID player, "bank", false]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "phone::bank::transfer::request": {
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
private _target = _data getOrDefault ["target", ""];
|
||||
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
|
||||
|
||||
if (_target isNotEqualTo "" && { _amount > 0 }) then {
|
||||
["forge_server_bank_requestTransfer", [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||
} else {
|
||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||
if !(isNull _display) then {
|
||||
private _control = _display displayCtrl 1001;
|
||||
if !(isNull _control) then {
|
||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Choose a recipient and valid amount.')"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
case "phone::bank::depositEarnings::request": {
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
|
||||
if (_amount > 0) then {
|
||||
["forge_server_bank_requestDepositEarnings", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
} else {
|
||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||
if !(isNull _display) then {
|
||||
private _control = _display displayCtrl 1001;
|
||||
if !(isNull _control) then {
|
||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid earnings amount.')"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
case "phone::bank::repayCreditLine::request": {
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
|
||||
if (_amount > 0) then {
|
||||
["forge_server_bank_requestRepayCreditLine", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
} else {
|
||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||
if !(isNull _display) then {
|
||||
private _control = _display displayCtrl 1001;
|
||||
if !(isNull _control) then {
|
||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid payment amount.')"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
default { hint format ["Unhandled phone event: %1", _event]; };
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1529
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
1529
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
BIN
arma/client/addons/phone/ui/_site/images/dark/Wallet.png
Normal file
BIN
arma/client/addons/phone/ui/_site/images/dark/Wallet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
arma/client/addons/phone/ui/_site/images/light/Wallet.png
Normal file
BIN
arma/client/addons/phone/ui/_site/images/light/Wallet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
@ -90,6 +90,9 @@ class App extends Component {
|
||||
case 'settings':
|
||||
window.initializeSettingsApp(appContainer);
|
||||
break;
|
||||
case 'wallet':
|
||||
window.initializeMobileBankApp(appContainer);
|
||||
break;
|
||||
default:
|
||||
return this.renderPlaceholderApp(currentApp);
|
||||
}
|
||||
@ -111,7 +114,8 @@ class App extends Component {
|
||||
mail: '',
|
||||
icloud: '',
|
||||
photos: '',
|
||||
safari: ''
|
||||
safari: '',
|
||||
wallet: ''
|
||||
};
|
||||
|
||||
return this.createElement(
|
||||
|
||||
436
arma/client/addons/phone/ui/_site/js/apps/wallet/index.js
Normal file
436
arma/client/addons/phone/ui/_site/js/apps/wallet/index.js
Normal file
@ -0,0 +1,436 @@
|
||||
/** @format */
|
||||
|
||||
let lastMobileBankRequest = 0;
|
||||
let mobileBankNoticeTimer = null;
|
||||
const MOBILE_BANK_REQUEST_COOLDOWN = 1000;
|
||||
|
||||
function defaultMobileBankState() {
|
||||
return {
|
||||
account: {
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
earnings: 0,
|
||||
transactions: [],
|
||||
},
|
||||
session: {
|
||||
creditLine: {
|
||||
amountDue: 0,
|
||||
approvedAmount: 0,
|
||||
availableAmount: 0,
|
||||
outstandingPrincipal: 0,
|
||||
},
|
||||
orgName: '',
|
||||
playerName: '',
|
||||
transferTargets: [],
|
||||
uid: '',
|
||||
},
|
||||
notice: null,
|
||||
pendingAction: '',
|
||||
};
|
||||
}
|
||||
|
||||
function getMobileBankState() {
|
||||
return {
|
||||
...defaultMobileBankState(),
|
||||
...(globalState.getState().mobileBank || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function setMobileBankState(patch) {
|
||||
globalState.setState({
|
||||
mobileBank: {
|
||||
...getMobileBankState(),
|
||||
...patch,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function formatMobileBankCurrency(value) {
|
||||
const amount = Math.floor(Number(value || 0));
|
||||
return `$${Math.max(0, amount).toLocaleString()}`;
|
||||
}
|
||||
|
||||
function normalizeMobileBankAmount(value) {
|
||||
const amount = Math.floor(Number(value || 0));
|
||||
return Number.isFinite(amount) ? amount : 0;
|
||||
}
|
||||
|
||||
function sendMobileBankEvent(event, data = {}) {
|
||||
if (typeof A3API !== 'undefined' && A3API.SendAlert) {
|
||||
A3API.SendAlert(JSON.stringify({ event, data }));
|
||||
return true;
|
||||
}
|
||||
|
||||
showMobileBankNotice('error', 'Bank bridge is unavailable.');
|
||||
return false;
|
||||
}
|
||||
|
||||
function requestMobileBankRefresh(force = false) {
|
||||
const now = Date.now();
|
||||
if (!force && now - lastMobileBankRequest < MOBILE_BANK_REQUEST_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastMobileBankRequest = now;
|
||||
return sendMobileBankEvent('phone::bank::refresh', {});
|
||||
}
|
||||
|
||||
function requestMobileBankTransfer(target, amountValue) {
|
||||
const targetUid = String(target || '').trim();
|
||||
const amount = normalizeMobileBankAmount(amountValue);
|
||||
|
||||
if (!targetUid) {
|
||||
showMobileBankNotice('error', 'Choose a recipient.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
showMobileBankNotice('error', 'Enter a valid transfer amount.');
|
||||
return false;
|
||||
}
|
||||
|
||||
setMobileBankState({ pendingAction: 'transfer' });
|
||||
const sent = sendMobileBankEvent('phone::bank::transfer::request', {
|
||||
amount,
|
||||
from: 'bank',
|
||||
target: targetUid,
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
setMobileBankState({ pendingAction: '' });
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
function requestMobileBankDepositEarnings() {
|
||||
const state = getMobileBankState();
|
||||
const availableEarnings = normalizeMobileBankAmount(state.account.earnings);
|
||||
|
||||
if (availableEarnings <= 0) {
|
||||
showMobileBankNotice('error', 'No earnings are available to deposit.');
|
||||
return false;
|
||||
}
|
||||
|
||||
setMobileBankState({ pendingAction: 'depositearnings' });
|
||||
const sent = sendMobileBankEvent('phone::bank::depositEarnings::request', {
|
||||
amount: availableEarnings,
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
setMobileBankState({ pendingAction: '' });
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
function requestMobileBankRepayCreditLine(amountValue) {
|
||||
const amount = normalizeMobileBankAmount(amountValue);
|
||||
const state = getMobileBankState();
|
||||
const amountDue = normalizeMobileBankAmount(state.session.creditLine?.amountDue);
|
||||
|
||||
if (amountDue <= 0) {
|
||||
showMobileBankNotice('error', 'No credit line payment is due.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
showMobileBankNotice('error', 'Enter a valid payment amount.');
|
||||
return false;
|
||||
}
|
||||
|
||||
setMobileBankState({ pendingAction: 'repaycreditline' });
|
||||
const sent = sendMobileBankEvent('phone::bank::repayCreditLine::request', {
|
||||
amount: Math.min(amount, amountDue),
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
setMobileBankState({ pendingAction: '' });
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
function updateMobileBank(payload) {
|
||||
const current = getMobileBankState();
|
||||
setMobileBankState({
|
||||
account: {
|
||||
...current.account,
|
||||
...(payload && payload.account ? payload.account : {}),
|
||||
},
|
||||
session: {
|
||||
...current.session,
|
||||
...(payload && payload.session ? payload.session : {}),
|
||||
},
|
||||
pendingAction: '',
|
||||
});
|
||||
}
|
||||
|
||||
function updateMobileBankAccount(accountPatch) {
|
||||
const current = getMobileBankState();
|
||||
setMobileBankState({
|
||||
account: {
|
||||
...current.account,
|
||||
...(accountPatch || {}),
|
||||
},
|
||||
pendingAction: '',
|
||||
});
|
||||
}
|
||||
|
||||
function showMobileBankNotice(type, message) {
|
||||
if (!message) return;
|
||||
|
||||
setMobileBankState({
|
||||
notice: {
|
||||
type: type || 'info',
|
||||
message,
|
||||
},
|
||||
pendingAction: '',
|
||||
});
|
||||
|
||||
if (mobileBankNoticeTimer) {
|
||||
clearTimeout(mobileBankNoticeTimer);
|
||||
}
|
||||
|
||||
mobileBankNoticeTimer = setTimeout(() => {
|
||||
setMobileBankState({ notice: null });
|
||||
mobileBankNoticeTimer = null;
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
function mobileBankTransactionRows(transactions) {
|
||||
const rows = Array.isArray(transactions) ? transactions.slice(0, 5) : [];
|
||||
|
||||
if (rows.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'wallet-empty-state';
|
||||
empty.textContent = 'No recent transactions';
|
||||
return empty;
|
||||
}
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'wallet-transaction-list';
|
||||
|
||||
rows.forEach((entry) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'wallet-transaction-row';
|
||||
|
||||
const copy = document.createElement('div');
|
||||
copy.className = 'wallet-transaction-copy';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'wallet-transaction-title';
|
||||
title.textContent = entry.type || 'Transaction';
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'wallet-transaction-meta';
|
||||
meta.textContent = entry.date || 'Pending timestamp';
|
||||
|
||||
const value = document.createElement('span');
|
||||
value.className = 'wallet-transaction-value';
|
||||
value.textContent = formatMobileBankCurrency(entry.amount || 0);
|
||||
|
||||
copy.append(title, meta);
|
||||
row.append(copy, value);
|
||||
list.appendChild(row);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function initializeMobileBankApp(container) {
|
||||
const state = getMobileBankState();
|
||||
const { account, session, notice, pendingAction } = state;
|
||||
const transferTargets = Array.isArray(session.transferTargets)
|
||||
? session.transferTargets
|
||||
: [];
|
||||
const creditLine = session.creditLine || {};
|
||||
const amountDue = normalizeMobileBankAmount(creditLine.amountDue);
|
||||
const outstandingPrincipal = normalizeMobileBankAmount(creditLine.outstandingPrincipal);
|
||||
|
||||
requestMobileBankRefresh(false);
|
||||
|
||||
const appContainer = document.createElement('div');
|
||||
appContainer.className = 'app-container wallet-app';
|
||||
appContainer.setAttribute('role', 'main');
|
||||
appContainer.setAttribute('aria-label', 'Wallet');
|
||||
|
||||
const navBar = new NavigationBar({
|
||||
title: 'Wallet',
|
||||
rightButton: {
|
||||
element: 'button',
|
||||
props: {
|
||||
className: 'wallet-nav-button',
|
||||
type: 'button',
|
||||
disabled: pendingAction !== '',
|
||||
onClick: () => requestMobileBankRefresh(true),
|
||||
'aria-label': 'Refresh wallet',
|
||||
},
|
||||
content: 'Refresh',
|
||||
},
|
||||
});
|
||||
navBar.mount(appContainer);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'content wallet-content';
|
||||
|
||||
if (notice && notice.message) {
|
||||
const noticeElement = document.createElement('div');
|
||||
noticeElement.className = `wallet-notice wallet-notice-${notice.type || 'info'}`;
|
||||
noticeElement.textContent = notice.message;
|
||||
content.appendChild(noticeElement);
|
||||
}
|
||||
|
||||
const hero = document.createElement('section');
|
||||
hero.className = 'wallet-balance-card';
|
||||
hero.innerHTML = `
|
||||
<span class="wallet-eyebrow">Available Balance</span>
|
||||
<strong class="wallet-balance">${formatMobileBankCurrency(account.bank)}</strong>
|
||||
<span class="wallet-owner">${session.playerName || 'Personal account'}</span>
|
||||
`;
|
||||
content.appendChild(hero);
|
||||
|
||||
const metrics = document.createElement('section');
|
||||
metrics.className = 'wallet-metrics';
|
||||
metrics.innerHTML = `
|
||||
<div class="wallet-metric">
|
||||
<span>Cash</span>
|
||||
<strong>${formatMobileBankCurrency(account.cash)}</strong>
|
||||
</div>
|
||||
<div class="wallet-metric">
|
||||
<span>Earnings</span>
|
||||
<strong>${formatMobileBankCurrency(account.earnings)}</strong>
|
||||
</div>
|
||||
`;
|
||||
content.appendChild(metrics);
|
||||
|
||||
const bankingActions = document.createElement('section');
|
||||
bankingActions.className = 'wallet-card';
|
||||
|
||||
const bankingTitle = document.createElement('div');
|
||||
bankingTitle.className = 'wallet-card-title';
|
||||
bankingTitle.textContent = 'Account Actions';
|
||||
|
||||
const earningsAction = document.createElement('div');
|
||||
earningsAction.className = 'wallet-action-block';
|
||||
|
||||
const earningsSummary = document.createElement('div');
|
||||
earningsSummary.className = 'wallet-action-summary';
|
||||
earningsSummary.innerHTML = `
|
||||
<span>Deposit Earnings</span>
|
||||
<strong>${formatMobileBankCurrency(account.earnings)} available</strong>
|
||||
<small>Move mission earnings into your bank balance.</small>
|
||||
`;
|
||||
|
||||
const earningsButton = document.createElement('button');
|
||||
earningsButton.className = 'wallet-secondary-button wallet-full-button';
|
||||
earningsButton.type = 'button';
|
||||
earningsButton.disabled = pendingAction !== '' || normalizeMobileBankAmount(account.earnings) <= 0;
|
||||
earningsButton.textContent = pendingAction === 'depositearnings' ? 'Depositing...' : 'Deposit Earnings';
|
||||
earningsButton.addEventListener('click', () => {
|
||||
requestMobileBankDepositEarnings();
|
||||
});
|
||||
earningsAction.append(earningsSummary, earningsButton);
|
||||
|
||||
const creditAction = document.createElement('div');
|
||||
creditAction.className = 'wallet-action-block';
|
||||
|
||||
const creditSummary = document.createElement('div');
|
||||
creditSummary.className = 'wallet-action-summary';
|
||||
creditSummary.innerHTML = `
|
||||
<span>Credit Line Payment</span>
|
||||
<strong>${formatMobileBankCurrency(amountDue)} due</strong>
|
||||
<small>${session.orgName || 'Organization'} - ${formatMobileBankCurrency(outstandingPrincipal)} outstanding</small>
|
||||
`;
|
||||
|
||||
const creditControls = document.createElement('div');
|
||||
creditControls.className = 'wallet-action-controls';
|
||||
|
||||
const creditAmount = document.createElement('input');
|
||||
creditAmount.className = 'wallet-input';
|
||||
creditAmount.type = 'number';
|
||||
creditAmount.min = '1';
|
||||
creditAmount.step = '1';
|
||||
creditAmount.placeholder = amountDue > 0 ? 'Payment amount' : 'No payment due';
|
||||
creditAmount.setAttribute('aria-label', 'Credit line payment amount');
|
||||
creditAmount.inputMode = 'numeric';
|
||||
creditAmount.disabled = pendingAction !== '' || amountDue <= 0;
|
||||
|
||||
const creditButton = document.createElement('button');
|
||||
creditButton.className = 'wallet-secondary-button';
|
||||
creditButton.type = 'button';
|
||||
creditButton.disabled = pendingAction !== '' || amountDue <= 0;
|
||||
creditButton.textContent = pendingAction === 'repaycreditline' ? 'Paying...' : 'Pay Credit';
|
||||
creditButton.addEventListener('click', () => {
|
||||
requestMobileBankRepayCreditLine(creditAmount.value || amountDue);
|
||||
});
|
||||
|
||||
creditControls.append(creditAmount, creditButton);
|
||||
creditAction.append(creditSummary, creditControls);
|
||||
bankingActions.append(bankingTitle, earningsAction, creditAction);
|
||||
content.appendChild(bankingActions);
|
||||
|
||||
const transferCard = document.createElement('section');
|
||||
transferCard.className = 'wallet-card';
|
||||
|
||||
const transferTitle = document.createElement('div');
|
||||
transferTitle.className = 'wallet-card-title';
|
||||
transferTitle.textContent = 'Transfer';
|
||||
|
||||
const targetSelect = document.createElement('select');
|
||||
targetSelect.className = 'wallet-input';
|
||||
targetSelect.setAttribute('aria-label', 'Transfer recipient');
|
||||
targetSelect.disabled = pendingAction !== '' || transferTargets.length === 0;
|
||||
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
placeholder.textContent = transferTargets.length === 0 ? 'No online recipients' : 'Choose recipient';
|
||||
targetSelect.appendChild(placeholder);
|
||||
|
||||
transferTargets.forEach((target) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = target.uid || '';
|
||||
option.textContent = target.name || target.uid || 'Player';
|
||||
targetSelect.appendChild(option);
|
||||
});
|
||||
|
||||
const amountInput = document.createElement('input');
|
||||
amountInput.className = 'wallet-input';
|
||||
amountInput.type = 'number';
|
||||
amountInput.min = '1';
|
||||
amountInput.step = '1';
|
||||
amountInput.placeholder = 'Amount';
|
||||
amountInput.inputMode = 'numeric';
|
||||
amountInput.disabled = pendingAction !== '';
|
||||
|
||||
const transferButton = document.createElement('button');
|
||||
transferButton.className = 'wallet-primary-button';
|
||||
transferButton.type = 'button';
|
||||
transferButton.disabled = pendingAction !== '' || transferTargets.length === 0;
|
||||
transferButton.textContent = pendingAction === 'transfer' ? 'Sending...' : 'Send Transfer';
|
||||
transferButton.addEventListener('click', () => {
|
||||
requestMobileBankTransfer(targetSelect.value, amountInput.value);
|
||||
});
|
||||
|
||||
transferCard.append(transferTitle, targetSelect, amountInput, transferButton);
|
||||
content.appendChild(transferCard);
|
||||
|
||||
const historyCard = document.createElement('section');
|
||||
historyCard.className = 'wallet-card';
|
||||
|
||||
const historyTitle = document.createElement('div');
|
||||
historyTitle.className = 'wallet-card-title';
|
||||
historyTitle.textContent = 'Recent Activity';
|
||||
|
||||
historyCard.append(historyTitle, mobileBankTransactionRows(account.transactions));
|
||||
content.appendChild(historyCard);
|
||||
|
||||
appContainer.appendChild(content);
|
||||
container.appendChild(appContainer);
|
||||
}
|
||||
|
||||
window.initializeMobileBankApp = initializeMobileBankApp;
|
||||
window.requestMobileBankRefresh = requestMobileBankRefresh;
|
||||
window.updateMobileBank = updateMobileBank;
|
||||
window.updateMobileBankAccount = updateMobileBankAccount;
|
||||
window.showMobileBankNotice = showMobileBankNotice;
|
||||
@ -94,6 +94,7 @@ class HomeScreen extends Component {
|
||||
{ name: 'photos', title: 'Photos', icon: 'Photos', color: '' },
|
||||
{ name: 'clock', title: 'Clock', icon: 'Clock', color: '' },
|
||||
{ name: 'calendar', title: 'Calendar', icon: 'Calendar', color: '' },
|
||||
{ name: 'wallet', title: 'Wallet', icon: 'Wallet', color: '' },
|
||||
{ name: 'store', title: 'App Store', icon: 'AppStore', color: '' },
|
||||
];
|
||||
}
|
||||
|
||||
@ -49,6 +49,23 @@ const initialAppState = {
|
||||
events: [],
|
||||
currentEvent: null,
|
||||
showEventEditor: false,
|
||||
|
||||
// Mobile bank state
|
||||
mobileBank: {
|
||||
account: {
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
earnings: 0,
|
||||
transactions: [],
|
||||
},
|
||||
session: {
|
||||
playerName: '',
|
||||
transferTargets: [],
|
||||
uid: '',
|
||||
},
|
||||
notice: null,
|
||||
pendingAction: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
314
arma/client/addons/phone/ui/_site/styles/components/wallet.css
Normal file
314
arma/client/addons/phone/ui/_site/styles/components/wallet.css
Normal file
@ -0,0 +1,314 @@
|
||||
/* Wallet */
|
||||
.wallet-app {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: calc(100% - 44px);
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px 28px;
|
||||
}
|
||||
|
||||
.wallet-nav-button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #275a8c;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-nav-button {
|
||||
color: #8bb9e6;
|
||||
}
|
||||
|
||||
.wallet-notice {
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.wallet-notice-success {
|
||||
background: rgba(47, 125, 91, 0.16);
|
||||
color: #2f7d5b;
|
||||
}
|
||||
|
||||
.wallet-notice-error {
|
||||
background: rgba(196, 57, 57, 0.16);
|
||||
color: #b42323;
|
||||
}
|
||||
|
||||
.wallet-notice-info {
|
||||
background: rgba(39, 90, 140, 0.14);
|
||||
color: #275a8c;
|
||||
}
|
||||
|
||||
.wallet-balance-card {
|
||||
background: linear-gradient(160deg, #142f52 0%, #275a8c 58%, #4f86bd 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 14px 26px rgba(20, 47, 82, 0.22);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 142px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.wallet-eyebrow,
|
||||
.wallet-card-title,
|
||||
.wallet-metric span,
|
||||
.wallet-transaction-meta {
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wallet-eyebrow {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.wallet-balance {
|
||||
font-size: 34px;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.wallet-owner {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 13px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.wallet-metrics {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wallet-metric,
|
||||
.wallet-card {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%);
|
||||
border: 1px solid rgba(18, 54, 93, 0.12);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-metric,
|
||||
[data-theme="dark"] .wallet-card {
|
||||
background: linear-gradient(180deg, #1c1c1e 0%, #151b23 100%);
|
||||
border-color: rgba(139, 185, 230, 0.18);
|
||||
}
|
||||
|
||||
.wallet-metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wallet-metric span,
|
||||
.wallet-card-title {
|
||||
color: #6f86a3;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-metric span,
|
||||
[data-theme="dark"] .wallet-card-title,
|
||||
[data-theme="dark"] .wallet-transaction-meta {
|
||||
color: #8ea2bb;
|
||||
}
|
||||
|
||||
.wallet-metric strong {
|
||||
color: #142f52;
|
||||
font-size: 18px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-metric strong {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.wallet-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.wallet-input {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid rgba(18, 54, 93, 0.16);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
min-height: 42px;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-input {
|
||||
border-color: var(--input-border);
|
||||
}
|
||||
|
||||
.wallet-primary-button {
|
||||
background: #275a8c;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.wallet-secondary-button {
|
||||
background: rgba(39, 90, 140, 0.12);
|
||||
border: 1px solid rgba(39, 90, 140, 0.18);
|
||||
border-radius: 12px;
|
||||
color: #275a8c;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
min-height: 42px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.wallet-full-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-secondary-button {
|
||||
background: rgba(139, 185, 230, 0.13);
|
||||
border-color: rgba(139, 185, 230, 0.2);
|
||||
color: #8bb9e6;
|
||||
}
|
||||
|
||||
.wallet-action-block {
|
||||
background: rgba(39, 90, 140, 0.08);
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.wallet-action-block + .wallet-action-block {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-action-block {
|
||||
background: rgba(139, 185, 230, 0.1);
|
||||
}
|
||||
|
||||
.wallet-action-summary {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.wallet-action-summary span {
|
||||
color: #6f86a3;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wallet-action-summary strong {
|
||||
color: #142f52;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.wallet-action-summary small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-action-summary span,
|
||||
[data-theme="dark"] .wallet-action-summary strong {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.wallet-action-controls {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(0, 1fr) 112px;
|
||||
}
|
||||
|
||||
.wallet-primary-button:disabled,
|
||||
.wallet-secondary-button:disabled,
|
||||
.wallet-nav-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.wallet-transaction-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wallet-transaction-row {
|
||||
align-items: center;
|
||||
border-top: 1px solid rgba(18, 54, 93, 0.1);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-transaction-row {
|
||||
border-top-color: rgba(139, 185, 230, 0.14);
|
||||
}
|
||||
|
||||
.wallet-transaction-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wallet-transaction-title {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wallet-transaction-meta {
|
||||
color: #6f86a3;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wallet-transaction-value {
|
||||
color: #142f52;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wallet-transaction-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.wallet-empty-state {
|
||||
border: 1px dashed rgba(18, 54, 93, 0.18);
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
@ -18,9 +18,12 @@ const files = [
|
||||
'../styles/components/contacts.css',
|
||||
'../styles/components/dialpad.css',
|
||||
'../styles/components/messages.css',
|
||||
'../styles/components/mail.css',
|
||||
'../styles/components/settings.css',
|
||||
'../styles/components/notes.css',
|
||||
'../styles/components/calendar.css',
|
||||
'../styles/components/clock.css',
|
||||
'../styles/components/wallet.css',
|
||||
'../styles/components/loader.css'
|
||||
];
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ const files = [
|
||||
|
||||
// Utils
|
||||
'../js/utils/helpers.js',
|
||||
'../js/utils/PhoneMedia.js',
|
||||
|
||||
// Shared Components
|
||||
'../js/components/StatusBar.js',
|
||||
@ -34,6 +35,12 @@ const files = [
|
||||
'../js/apps/messages/components/ConversationView.js',
|
||||
'../js/apps/messages/index.js',
|
||||
|
||||
// Mail App
|
||||
'../js/apps/mail/components/MailList.js',
|
||||
'../js/apps/mail/components/MailDetail.js',
|
||||
'../js/apps/mail/components/MailComposer.js',
|
||||
'../js/apps/mail/index.js',
|
||||
|
||||
// Contacts App
|
||||
'../js/apps/contacts/components/ContactList.js',
|
||||
'../js/apps/contacts/components/ContactItem.js',
|
||||
@ -56,6 +63,13 @@ const files = [
|
||||
'../js/apps/clock/components/AlarmClock.js',
|
||||
'../js/apps/clock/index.js',
|
||||
|
||||
// Calendar App
|
||||
'../js/apps/calendar/components/Calendar.js',
|
||||
'../js/apps/calendar/components/EventEditor.js',
|
||||
'../js/apps/calendar/index.js',
|
||||
|
||||
// Wallet App
|
||||
'../js/apps/wallet/index.js',
|
||||
|
||||
// Main App
|
||||
'../js/app.js',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user