/// Format a remaining-seconds countdown for display in a button or label. /// - Under 90 seconds: "Xd" (e.g. "60d") /// - 90 seconds and up: "Xm Yd" (e.g. "11m 40d") /// `d` and `m` are Indonesian short forms for detik (second) and menit (minute). String formatCountdown(int totalSeconds) { if (totalSeconds < 90) return '${totalSeconds}d'; final minutes = totalSeconds ~/ 60; final seconds = totalSeconds % 60; return '${minutes}m ${seconds}d'; } /// Format an integer rupiah amount with dot thousand-separators: 1234567 → "Rp 1.234.567". String formatRupiah(int amount) { final str = amount.toString(); final buffer = StringBuffer(); for (var i = 0; i < str.length; i++) { if (i > 0 && (str.length - i) % 3 == 0) buffer.write('.'); buffer.write(str[i]); } return 'Rp $buffer'; } /// User types class UserType { static const customer = 'customer'; static const mitra = 'mitra'; UserType._(); } /// Chat session statuses class SessionStatus { static const searching = 'searching'; static const pendingAcceptance = 'pending_acceptance'; static const pendingPayment = 'pending_payment'; static const active = 'active'; static const extending = 'extending'; static const closing = 'closing'; static const completed = 'completed'; static const cancelled = 'cancelled'; static const expired = 'expired'; SessionStatus._(); } /// Chat message statuses class MessageStatus { static const sent = 'sent'; static const delivered = 'delivered'; static const read = 'read'; MessageStatus._(); } /// Chat message types class MessageType { static const text = 'text'; MessageType._(); } /// Session extension statuses class ExtensionStatus { static const pending = 'pending'; static const accepted = 'accepted'; static const rejected = 'rejected'; static const timeout = 'timeout'; ExtensionStatus._(); } /// Session topic sensitivity enum TopicSensitivity { regular('regular'), sensitive('sensitive'); final String value; const TopicSensitivity(this.value); static TopicSensitivity fromString(String? v) => values.firstWhere((e) => e.value == v, orElse: () => TopicSensitivity.regular); } /// WebSocket message types class WsMessage { // Auth static const auth = 'auth'; static const authOk = 'auth_ok'; static const error = 'error'; // Chat static const message = 'message'; static const messageAck = 'message_ack'; static const messageStatus = 'message_status'; static const typing = 'typing'; // Pairing static const chatRequest = 'chat_request'; static const chatRequestClosed = 'chat_request_closed'; static const paired = 'paired'; // Session lifecycle static const sessionTimer = 'session_timer'; static const sessionExpired = 'session_expired'; static const sessionClosing = 'session_closing'; static const sessionCompleted = 'session_completed'; static const sessionPaused = 'session_paused'; static const sessionResumed = 'session_resumed'; // Extension static const extensionRequest = 'extension_request'; static const extensionResponse = 'extension_response'; // Topic sensitivity static const sessionTopicUpdated = 'session_topic_updated'; // Delivery static const delivered = 'delivered'; static const read = 'read'; // Early end static const earlyEnd = 'early_end'; // Returning-chat (intermediate failures — payment stays confirmed) static const returningChatTimeout = 'returning_chat_timeout'; static const returningChatRejected = 'returning_chat_rejected'; // Terminal pairing failure on a confirmed payment session static const pairingFailed = 'pairing_failed'; WsMessage._(); } /// Pairing-failure cause tags. Mirror of backend /// `PairingFailureCause` (see backend/src/constants.js). Use for both routing /// (terminal vs. intermediate) and surfacing copy on the failed-pairing screen. enum PairingFailureCause { noMitraAvailable('no_mitra_available'), allMitrasRejected('all_mitras_rejected'), targetedMitraOffline('targeted_mitra_offline'), targetedMitraRejected('targeted_mitra_rejected'), targetedMitraTimeout('targeted_mitra_timeout'), paymentSessionExpired('payment_session_expired'), customerCancelled('customer_cancelled'), unknown('unknown'); final String value; const PairingFailureCause(this.value); static PairingFailureCause fromString(String? v) => values.firstWhere((e) => e.value == v, orElse: () => PairingFailureCause.unknown); } /// Payment session lifecycle. Mirror of backend /// `PaymentSessionStatus`. class PaymentSessionStatus { static const pending = 'pending'; static const confirmed = 'confirmed'; static const consumed = 'consumed'; static const failedPairing = 'failed_pairing'; static const abandoned = 'abandoned'; static const expired = 'expired'; PaymentSessionStatus._(); }