From a1dd18ebdfffadd1a3d32ec6cad92655d6bed79b Mon Sep 17 00:00:00 2001 From: chukwudiikeh Date: Tue, 28 Apr 2026 06:39:23 +0000 Subject: [PATCH 1/4] fix(a11y): Add aria-live region for payment success/error messages - Move aria-live from individual Message components to StatusMessage wrapper - Set aria-live='polite' on container with aria-atomic='false' for proper announcement - Remove aria-live from Message components to avoid duplicate announcements - Ensures all msg.success, msg.error, and msg.info calls are announced by screen readers --- frontend/src/components/StatusMessage.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/StatusMessage.jsx b/frontend/src/components/StatusMessage.jsx index c3cdcb8..79ad421 100644 --- a/frontend/src/components/StatusMessage.jsx +++ b/frontend/src/components/StatusMessage.jsx @@ -16,7 +16,6 @@ function Message({ msg, onRemove, onRetry }) { initial="hidden" animate="visible" exit="exit" layout role="alert" - aria-live={msg.type === 'error' ? 'assertive' : 'polite'} aria-atomic="true" > @@ -34,7 +33,7 @@ export function StatusMessage({ messages, onRemove, showHistory = false, history const [historyOpen, setHistoryOpen] = useState(false); return ( -
+
{messages.map((msg) => ( From 8ba231a45e6871eb0f16270e5fdc44a70d3f137b Mon Sep 17 00:00:00 2001 From: chukwudiikeh Date: Tue, 28 Apr 2026 06:39:47 +0000 Subject: [PATCH 2/4] perf(db): Add database indexes for common query patterns - Transaction: Add indexes on (senderId, createdAt) and (recipientId, createdAt) for transaction history queries - KYCRecord: Add index on status for compliance dashboard queries - AMLAlert: Add indexes on userId and (severity, createdAt) for alert filtering and sorting - Improves query performance for transaction lookups, KYC status checks, and AML alert retrieval --- backend/prisma/schema.prisma | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 17184e7..f3707c0 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -41,6 +41,9 @@ model Transaction { lastRetryAt DateTime? retryHistory RetryAttempt[] amlAlerts AMLAlert[] + + @@index([senderId, createdAt]) + @@index([recipientId, createdAt]) } model RetryAttempt { @@ -142,6 +145,8 @@ model KYCRecord { riskLevel String? submittedAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([status]) } model AMLAlert { @@ -156,6 +161,9 @@ model AMLAlert { riskScore Int riskLevel String createdAt DateTime @default(now()) + + @@index([userId]) + @@index([severity, createdAt]) } model FeeBumpStat { From 94841f7fe22d79c952872d677c85122621c9be90 Mon Sep 17 00:00:00 2001 From: chukwudiikeh Date: Tue, 28 Apr 2026 06:40:07 +0000 Subject: [PATCH 3/4] feat(ux): Improve error messages for Stellar SDK errors - Add STELLAR_RESULT_CODES map with human-readable messages for transaction and operation result codes - Parse response.data.extras.result_codes from Stellar SDK errors - Check transaction and operation result codes before falling back to string matching - Provides clearer error messages for common Stellar failures (insufficient balance, no destination, etc.) - Improves user experience with specific, actionable error feedback --- frontend/src/utils/errorMessages.js | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/frontend/src/utils/errorMessages.js b/frontend/src/utils/errorMessages.js index 9d736ce..38f408c 100644 --- a/frontend/src/utils/errorMessages.js +++ b/frontend/src/utils/errorMessages.js @@ -7,7 +7,58 @@ const ERROR_MAP = [ { match: /tx_failed/i, message: 'Transaction was rejected by the Stellar network.' }, ]; +const STELLAR_RESULT_CODES = { + // Transaction result codes + tx_success: 'Transaction completed successfully.', + tx_failed: 'Transaction failed.', + tx_too_early: 'Transaction timestamp is too early.', + tx_too_late: 'Transaction timestamp is too late.', + tx_missing_operation: 'Transaction has no operations.', + tx_bad_seq: 'Transaction sequence number is invalid.', + tx_bad_auth: 'Transaction authentication failed.', + tx_insufficient_balance: 'Insufficient balance for this transaction.', + tx_no_source_account: 'Source account does not exist.', + tx_insufficient_fee: 'Transaction fee is too low.', + tx_fee_bump_inner_failed: 'Inner transaction of fee bump failed.', + tx_bad_auth_extra: 'Extra signers provided but not required.', + tx_internal_error: 'Internal Stellar network error.', + tx_not_supported: 'Transaction type is not supported.', + tx_bad_sponsorship: 'Sponsorship setup is invalid.', + tx_bad_min_seq_age: 'Minimum sequence age requirement not met.', + tx_malformed: 'Transaction is malformed.', + + // Operation result codes + op_success: 'Operation completed successfully.', + op_inner: 'Operation failed with inner error.', + op_bad_auth: 'Operation authentication failed.', + op_no_destination: 'Destination account does not exist.', + op_no_trust: 'Destination has no trust line for this asset.', + op_not_authorized: 'Operation not authorized.', + op_underfunded: 'Source account has insufficient funds.', + op_line_full: 'Destination trust line is full.', + op_self_not_allowed: 'Cannot send to self.', + op_not_supported: 'Operation type is not supported.', +}; + export function getFriendlyError(error) { + // Check for Stellar SDK result codes first + const resultCodes = error?.response?.data?.extras?.result_codes; + if (resultCodes) { + if (resultCodes.transaction) { + const txCode = resultCodes.transaction; + if (STELLAR_RESULT_CODES[txCode]) { + return STELLAR_RESULT_CODES[txCode]; + } + } + if (resultCodes.operations && resultCodes.operations.length > 0) { + const opCode = resultCodes.operations[0]; + if (STELLAR_RESULT_CODES[opCode]) { + return STELLAR_RESULT_CODES[opCode]; + } + } + } + + // Fall back to string matching const raw = error?.response?.data?.error || error?.message || String(error); console.error('[Stellar Error]', raw); const match = ERROR_MAP.find(e => e.match.test(raw)); From 306ddafe0f021b2ca70c3d6cf83ddf4396134db3 Mon Sep 17 00:00:00 2001 From: chukwudiikeh Date: Tue, 28 Apr 2026 06:40:57 +0000 Subject: [PATCH 4/4] refactor(ui): Replace inline styles with CSS classes in security key components - Move SecurityKeyWarning inline styles to BEM-based CSS classes - Move SecretKeyDisplay inline styles to BEM-based CSS classes - Add comprehensive CSS classes to index.css for both components - Improves maintainability, reduces component file size, and enables theming - Maintains responsive design with media queries for mobile devices --- .../src/components/SecurityKeyWarning.jsx | 201 ++++------------- frontend/src/index.css | 210 ++++++++++++++++++ 2 files changed, 251 insertions(+), 160 deletions(-) diff --git a/frontend/src/components/SecurityKeyWarning.jsx b/frontend/src/components/SecurityKeyWarning.jsx index ea44c63..74c1f53 100644 --- a/frontend/src/components/SecurityKeyWarning.jsx +++ b/frontend/src/components/SecurityKeyWarning.jsx @@ -13,77 +13,44 @@ export function SecurityKeyWarning({ onAcknowledge }) { initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} - style={{ - background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)', - border: '2px solid #ef4444', - borderRadius: 8, - padding: 16, - marginBottom: 16, - }} > - {/* Header */} -
- 🔐 +
+ 🔐
-

- Secret Key Security Alert -

-

- Your secret key is displayed. Keep it secure and private. -

+

Secret Key Security Alert

+

Your secret key is displayed. Keep it secure and private.

- {/* Warning list */} -
-
    -
  • - ⚠️ - Never share your secret key with anyone, including support staff -
  • -
  • - ⚠️ - Never paste your secret key into websites or applications you don't trust -
  • -
  • - ⚠️ - Store offline in a secure location (hardware wallet, encrypted file, etc.) -
  • -
  • - ⚠️ - Screenshot carefully and store in encrypted cloud storage only -
  • -
  • - ⚠️ - Anyone with this key can access and transfer all your funds -
  • -
-
+
    +
  • + ⚠️ + Never share your secret key with anyone, including support staff +
  • +
  • + ⚠️ + Never paste your secret key into websites or applications you don't trust +
  • +
  • + ⚠️ + Store offline in a secure location (hardware wallet, encrypted file, etc.) +
  • +
  • + ⚠️ + Screenshot carefully and store in encrypted cloud storage only +
  • +
  • + ⚠️ + Anyone with this key can access and transfer all your funds +
  • +
- {/* Action buttons */} -
+
onAcknowledge?.()} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} - style={{ - background: '#ef4444', - color: 'white', - border: 'none', - borderRadius: 4, - padding: '8px 12px', - fontSize: 13, - fontWeight: 600, - cursor: 'pointer', - flex: 1, - minWidth: 120, - }} + className="security-warning__button" > I Understand the Risks @@ -112,9 +79,9 @@ export function SecretKeyDisplay({ secretKey, publicKey }) { return ( {!acknowledged && ( setAcknowledged(true)} /> @@ -128,99 +95,32 @@ export function SecretKeyDisplay({ secretKey, publicKey }) { exit={{ opacity: 0, y: -8 }} style={{ marginBottom: 16 }} > -
- -
- - {publicKey} - +
+ +
+ {publicKey} handleCopy(publicKey, 'public')} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} - style={{ - background: 'white', - border: '1px solid #bfdbfe', - borderRadius: 4, - padding: '4px 8px', - fontSize: 12, - cursor: 'pointer', - width: 'auto', - minHeight: 'unset', - }} + className="secret-key-display__button" > {copied === 'public' ? '✓ Copied' : 'Copy'}
-
- -
- +
+ +
+ {revealed ? secretKey : masked} setRevealed(!revealed)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} - style={{ - background: revealed ? '#fecaca' : 'white', - color: revealed ? '#991b1b' : '#666', - border: '1px solid #ef4444', - borderRadius: 4, - padding: '4px 8px', - fontSize: 12, - cursor: 'pointer', - width: 'auto', - minHeight: 'unset', - fontWeight: 600, - }} + className="secret-key-display__button secret-key-display__button--reveal" > {revealed ? '👁 Hide' : '👁 Show'} @@ -229,18 +129,7 @@ export function SecretKeyDisplay({ secretKey, publicKey }) { whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} disabled={!revealed} - style={{ - background: revealed ? '#ef4444' : '#e5e7eb', - color: revealed ? 'white' : '#999', - border: 'none', - borderRadius: 4, - padding: '4px 8px', - fontSize: 12, - cursor: revealed ? 'pointer' : 'not-allowed', - width: 'auto', - minHeight: 'unset', - fontWeight: 600, - }} + className="secret-key-display__button secret-key-display__button--copy" > {copied === 'secret' ? '✓ Copied' : 'Copy'} @@ -248,18 +137,10 @@ export function SecretKeyDisplay({ secretKey, publicKey }) {
💡 Tip: Save both keys somewhere secure before leaving this page. They will not be displayed again. diff --git a/frontend/src/index.css b/frontend/src/index.css index 2beb6be..7be2721 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1918,3 +1918,213 @@ kbd { font-size: 0.8rem; } } + + +/* ── Security Key Warning & Display ──────────────────────────────────────── */ + +.security-warning { + background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); + border: 2px solid #ef4444; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.security-warning__header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; +} + +.security-warning__icon { + font-size: 24px; + flex-shrink: 0; +} + +.security-warning__title { + margin: 0 0 4px 0; + color: #991b1b; + font-size: 16px; + font-weight: 600; +} + +.security-warning__subtitle { + margin: 0; + font-size: 12px; + color: #7f1d1d; +} + +.security-warning__list { + background: white; + border-radius: 6px; + padding: 12px; + margin-bottom: 12px; + list-style: none; + padding-left: 0; + margin-top: 0; + font-size: 13px; + color: #7f1d1d; +} + +.security-warning__list-item { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.security-warning__list-item:last-child { + margin-bottom: 0; +} + +.security-warning__list-icon { + flex-shrink: 0; +} + +.security-warning__actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.security-warning__button { + background: #ef4444; + color: white; + border: none; + border-radius: 4px; + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + flex: 1; + min-width: 120px; + transition: background 0.2s; +} + +.security-warning__button:hover { + background: #dc2626; +} + +.secret-key-display { + margin-top: 16px; +} + +.secret-key-display__field { + margin-bottom: 12px; +} + +.secret-key-display__label { + display: block; + font-size: 12px; + font-weight: 600; + margin-bottom: 6px; + color: #555; +} + +.secret-key-display__input-group { + display: flex; + gap: 8px; + align-items: center; + border-radius: 4px; + padding: 10px; +} + +.secret-key-display__input-group--public { + background: #f0f9ff; + border: 1px solid #bfdbfe; +} + +.secret-key-display__input-group--secret { + background: #fef2f2; + border: 2px solid #ef4444; +} + +.secret-key-display__code { + flex: 1; + font-size: 12px; + overflow: auto; + font-family: monospace; + word-break: break-all; +} + +.secret-key-display__code--secret { + color: #991b1b; +} + +.secret-key-display__code--masked { + color: #999; +} + +.secret-key-display__button { + background: white; + border: 1px solid #bfdbfe; + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + width: auto; + min-height: unset; + transition: all 0.2s; +} + +.secret-key-display__button:hover { + background: #f0f9ff; +} + +.secret-key-display__button--reveal { + background: #fecaca; + color: #991b1b; + border: 1px solid #ef4444; + font-weight: 600; +} + +.secret-key-display__button--reveal:hover { + background: #fca5a5; +} + +.secret-key-display__button--copy { + background: #ef4444; + color: white; + border: none; + font-weight: 600; +} + +.secret-key-display__button--copy:hover { + background: #dc2626; +} + +.secret-key-display__button--copy:disabled { + background: #e5e7eb; + color: #999; + cursor: not-allowed; +} + +.secret-key-display__tip { + background: #fef3c7; + border: 1px solid #fbbf24; + border-radius: 4px; + padding: 10px; + margin-top: 10px; + font-size: 12px; + color: #78350f; +} + +@media (max-width: 480px) { + .security-warning__actions { + flex-direction: column; + } + + .security-warning__button { + width: 100%; + min-width: unset; + } + + .secret-key-display__input-group { + flex-wrap: wrap; + } + + .secret-key-display__button { + font-size: 11px; + padding: 3px 6px; + } +}