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:
Jacob Schmidt 2026-05-19 20:58:57 -05:00
parent 008631ed10
commit 5c76d59baf
16 changed files with 2511 additions and 868 deletions

View File

@ -3,6 +3,26 @@
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); }; if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); }; 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), { [QGVAR(initBank), {
GVAR(BankRepository) call ["init", []]; GVAR(BankRepository) call ["init", []];
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
@ -14,6 +34,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
if !(isNil QGVAR(BankUIBridge)) then { if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]]; GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
}; };
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(responseSyncBank), { [QGVAR(responseSyncBank), {
@ -23,6 +44,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
if !(isNil QGVAR(BankUIBridge)) then { if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]]; GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
}; };
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(responseHydrateBank), { [QGVAR(responseHydrateBank), {
@ -31,6 +53,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
if !(isNil QGVAR(BankUIBridge)) then { if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]]; GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
}; };
["updateMobileBank", [_data]] call GVAR(sendPhoneBankEvent);
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(responseBankNotice), { [QGVAR(responseBankNotice), {
@ -39,6 +62,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
if !(isNil QGVAR(BankUIBridge)) then { if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]]; GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
}; };
["showMobileBankNotice", [_type, _message]] call GVAR(sendPhoneBankEvent);
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[{ [{

View File

@ -345,6 +345,56 @@ switch (_event) do {
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms]; 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]; }; default { hint format ["Unhandled phone event: %1", _event]; };
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

View File

@ -90,6 +90,9 @@ class App extends Component {
case 'settings': case 'settings':
window.initializeSettingsApp(appContainer); window.initializeSettingsApp(appContainer);
break; break;
case 'wallet':
window.initializeMobileBankApp(appContainer);
break;
default: default:
return this.renderPlaceholderApp(currentApp); return this.renderPlaceholderApp(currentApp);
} }
@ -111,7 +114,8 @@ class App extends Component {
mail: '', mail: '',
icloud: '', icloud: '',
photos: '', photos: '',
safari: '' safari: '',
wallet: ''
}; };
return this.createElement( return this.createElement(

View 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;

View File

@ -94,6 +94,7 @@ class HomeScreen extends Component {
{ name: 'photos', title: 'Photos', icon: 'Photos', color: '' }, { name: 'photos', title: 'Photos', icon: 'Photos', color: '' },
{ name: 'clock', title: 'Clock', icon: 'Clock', color: '' }, { name: 'clock', title: 'Clock', icon: 'Clock', color: '' },
{ name: 'calendar', title: 'Calendar', icon: 'Calendar', color: '' }, { name: 'calendar', title: 'Calendar', icon: 'Calendar', color: '' },
{ name: 'wallet', title: 'Wallet', icon: 'Wallet', color: '' },
{ name: 'store', title: 'App Store', icon: 'AppStore', color: '' }, { name: 'store', title: 'App Store', icon: 'AppStore', color: '' },
]; ];
} }

View File

@ -49,6 +49,23 @@ const initialAppState = {
events: [], events: [],
currentEvent: null, currentEvent: null,
showEventEditor: false, showEventEditor: false,
// Mobile bank state
mobileBank: {
account: {
bank: 0,
cash: 0,
earnings: 0,
transactions: [],
},
session: {
playerName: '',
transferTargets: [],
uid: '',
},
notice: null,
pendingAction: '',
},
}; };
/** /**

View 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;
}

View File

@ -18,9 +18,12 @@ const files = [
'../styles/components/contacts.css', '../styles/components/contacts.css',
'../styles/components/dialpad.css', '../styles/components/dialpad.css',
'../styles/components/messages.css', '../styles/components/messages.css',
'../styles/components/mail.css',
'../styles/components/settings.css', '../styles/components/settings.css',
'../styles/components/notes.css', '../styles/components/notes.css',
'../styles/components/calendar.css',
'../styles/components/clock.css', '../styles/components/clock.css',
'../styles/components/wallet.css',
'../styles/components/loader.css' '../styles/components/loader.css'
]; ];

View File

@ -12,6 +12,7 @@ const files = [
// Utils // Utils
'../js/utils/helpers.js', '../js/utils/helpers.js',
'../js/utils/PhoneMedia.js',
// Shared Components // Shared Components
'../js/components/StatusBar.js', '../js/components/StatusBar.js',
@ -34,6 +35,12 @@ const files = [
'../js/apps/messages/components/ConversationView.js', '../js/apps/messages/components/ConversationView.js',
'../js/apps/messages/index.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 // Contacts App
'../js/apps/contacts/components/ContactList.js', '../js/apps/contacts/components/ContactList.js',
'../js/apps/contacts/components/ContactItem.js', '../js/apps/contacts/components/ContactItem.js',
@ -56,6 +63,13 @@ const files = [
'../js/apps/clock/components/AlarmClock.js', '../js/apps/clock/components/AlarmClock.js',
'../js/apps/clock/index.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 // Main App
'../js/app.js', '../js/app.js',