From 5f49a647fb8eda2ebe3ea544f3df51a6dbec3bf6 Mon Sep 17 00:00:00 2001 From: Mateus Andrade Date: Tue, 22 Jul 2025 19:04:58 -0300 Subject: [PATCH 1/4] Refactor App component and extract functionalities into reusable components - Simplified App component by extracting header, payment event, loading indicator, payment buttons, and transaction info into separate components. - Introduced a theme for consistent styling across the application. - Created a Button component for reusable button styles and states. - Added a Card component for consistent card styling. - Implemented a LoadingIndicator component for better loading state management. - Created PaymentButtons component to handle payment actions. - Added PaymentEvent component to display payment event messages. - Introduced TransactionInfo component to show details of the last transaction. - Refactored payment operations into a custom hook for better state management and separation of concerns. - Added utility functions for alert handling and async operations. - Defined constants for payment amounts and terminal activation code. --- example/src/App.tsx | 456 +++----------------- example/src/components/Button.tsx | 96 +++++ example/src/components/Card.tsx | 36 ++ example/src/components/Header.tsx | 50 +++ example/src/components/LoadingIndicator.tsx | 35 ++ example/src/components/PaymentButtons.tsx | 77 ++++ example/src/components/PaymentEvent.tsx | 54 +++ example/src/components/TransactionInfo.tsx | 76 ++++ example/src/components/index.ts | 7 + example/src/constants/index.ts | 1 + example/src/constants/theme.ts | 104 +++++ example/src/hooks/index.ts | 1 + example/src/hooks/usePaymentOperations.ts | 176 ++++++++ example/src/utils/alerts.ts | 16 + example/src/utils/asyncOperations.ts | 23 + example/src/utils/index.ts | 3 + example/src/utils/payment.ts | 106 +++++ 17 files changed, 916 insertions(+), 401 deletions(-) create mode 100644 example/src/components/Button.tsx create mode 100644 example/src/components/Card.tsx create mode 100644 example/src/components/Header.tsx create mode 100644 example/src/components/LoadingIndicator.tsx create mode 100644 example/src/components/PaymentButtons.tsx create mode 100644 example/src/components/PaymentEvent.tsx create mode 100644 example/src/components/TransactionInfo.tsx create mode 100644 example/src/components/index.ts create mode 100644 example/src/constants/index.ts create mode 100644 example/src/constants/theme.ts create mode 100644 example/src/hooks/index.ts create mode 100644 example/src/hooks/usePaymentOperations.ts create mode 100644 example/src/utils/alerts.ts create mode 100644 example/src/utils/asyncOperations.ts create mode 100644 example/src/utils/index.ts create mode 100644 example/src/utils/payment.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index 6723782..6d781cd 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,418 +1,72 @@ -import React, { useEffect, useState } from 'react'; +import { ScrollView, StyleSheet, StatusBar } from 'react-native'; +import { useTransactionEvent } from 'react-native-plugpag-nitro'; +import { theme } from './constants/theme'; +import { usePaymentOperations } from './hooks/usePaymentOperations'; import { - ActivityIndicator, - Alert, - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native'; -import { - type PlugpagTransactionResult, - initializeAndActivatePinPad, - doPayment, - refundPayment, - useTransactionEvent, - getTerminalSerialNumber, - PaymentType, - ErrorCode, - InstallmentType, -} from 'react-native-plugpag-nitro'; + Header, + PaymentEvent, + LoadingIndicator, + PaymentButtons, + TransactionInfo, +} from './components'; export default function App() { - const [isProcessing, setIsProcessing] = useState(false); - - const [terminalSerial, setTerminalSerial] = useState('N/A'); - - const [lastPayment, setLastPayment] = - useState(null); - - const [isInitialized, setIsInitialized] = useState(false); + const { + isProcessing, + isInitialized, + terminalSerial, + lastPayment, + handleInitialize, + handlePayment, + handleRefund, + } = usePaymentOperations(); // Real-time payment events const paymentEvent = useTransactionEvent(); - // Get terminal serial on mount - useEffect(() => { - try { - setTerminalSerial(getTerminalSerialNumber()); - } catch (e) { - console.warn(e); - } - }, []); - - // Initialize terminal - const handleInitialize = async () => { - setIsProcessing(true); - try { - const result = await initializeAndActivatePinPad('403938'); - if (result.result === ErrorCode.OK) { - setIsInitialized(true); - Alert.alert('✅ Sucesso', 'Terminal inicializado com sucesso'); - } else { - Alert.alert('❌ Erro', result.errorMessage || 'Falha ao inicializar'); - } - } catch (e: any) { - Alert.alert('❌ Error', e.message); - } - setIsProcessing(false); - }; - - // Credit payment - const handleCreditPayment = async () => { - if (!isInitialized) { - Alert.alert('⚠️ Aviso', 'Por favor, inicialize o terminal primeiro'); - return; - } - - setIsProcessing(true); - try { - const result = await doPayment({ - amount: 2500, // R$ 25.00 - type: PaymentType.CREDIT, - }); - setLastPayment(result); - - if (result.result === ErrorCode.OK) { - Alert.alert( - '✅ Pagamento Aprovado', - `Transação realizada com sucesso!\nCódigo: ${result.transactionCode}\nValor: R$ 25,00` - ); - } else { - Alert.alert( - '❌ Pagamento Negado', - result.message || 'Transação falhou' - ); - } - } catch (e: any) { - Alert.alert('❌ Erro', e.message); - } - setIsProcessing(false); - }; - - // Debit payment - const handleDebitPayment = async () => { - if (!isInitialized) { - Alert.alert('⚠️ Aviso', 'Por favor, inicialize o terminal primeiro'); - return; - } - - setIsProcessing(true); - try { - const result = await doPayment({ - amount: 1500, // R$ 15.00 - type: PaymentType.DEBIT, - }); - setLastPayment(result); - - if (result.result === ErrorCode.OK) { - Alert.alert( - '✅ Pagamento Aprovado', - `Transação de débito realizada com sucesso!\nCódigo: ${result.transactionCode}\nValor: R$ 15,00` - ); - } else { - Alert.alert( - '❌ Pagamento Negado', - result.message || 'Transação falhou' - ); - } - } catch (e: any) { - Alert.alert('❌ Erro', e.message); - } - setIsProcessing(false); - }; - - // Credit payment - const handlePIXPayment = async () => { - if (!isInitialized) { - Alert.alert('⚠️ Aviso', 'Por favor, inicialize o terminal primeiro'); - return; - } - - setIsProcessing(true); - try { - const result = await doPayment({ - amount: 1500, // R$ 15.00 - type: PaymentType.PIX, - }); - setLastPayment(result); - - if (result.result === ErrorCode.OK) { - Alert.alert( - '✅ Pagamento Aprovado', - `Transação realizada com sucesso!\nCódigo: ${result.transactionCode}\nValor: R$ 25,00` - ); - } else { - Alert.alert( - '❌ Pagamento Negado', - result.message || 'Transação falhou' - ); - } - } catch (e: any) { - Alert.alert('❌ Erro', e.message); - } - setIsProcessing(false); - }; - - // Installment payment - const handleInstallmentPayment = async () => { - if (!isInitialized) { - Alert.alert('⚠️ Aviso', 'Por favor, inicialize o terminal primeiro'); - return; - } - - setIsProcessing(true); - try { - const result = await doPayment({ - amount: 10000, // R$ 100.00 - type: PaymentType.CREDIT, - installmentType: InstallmentType.BUYER_INSTALLMENT, - installments: 3, - }); - setLastPayment(result); - - if (result.result === ErrorCode.OK) { - Alert.alert( - '✅ Pagamento Aprovado', - `Pagamento parcelado realizado com sucesso!\nCódigo: ${result.transactionCode}\nValor: R$ 100,00 (3x)` - ); - } else { - Alert.alert( - '❌ Pagamento Negado', - result.message || 'Transação falhou' - ); - } - } catch (e: any) { - Alert.alert('❌ Erro', e.message); - } - setIsProcessing(false); - }; - - // Refund last payment - const handleRefund = async () => { - if (!lastPayment?.transactionCode || !lastPayment.transactionId) { - Alert.alert('⚠️ Aviso', 'Nenhuma transação para estornar'); - return; - } - - setIsProcessing(true); - try { - const result = await refundPayment({ - transactionCode: lastPayment.transactionCode, - transactionId: lastPayment.transactionId, - }); - - if (result.result === ErrorCode.OK) { - Alert.alert( - '✅ Estorno Realizado', - `Código: ${result.transactionCode}` - ); - setLastPayment(null); - } else { - Alert.alert('❌ Estorno Falhou', result.message || 'Estorno falhou'); - } - } catch (e: any) { - Alert.alert('❌ Erro', e.message); - } - setIsProcessing(false); - }; - - const getEventColor = (code: number) => { - if (code >= 1001 && code <= 1004) return '#007AFF'; // Card events - Blue - if (code >= 1010 && code <= 1012) return '#FF9500'; // Password events - Orange - if (code >= 1020 && code <= 1023) return '#FFCC02'; // Processing - Yellow - if (code >= 1030 && code <= 1032) return '#34C759'; // Terminal response - Green - if (code >= 1040 && code <= 1043) return '#FF3B30'; // Errors - Red - return '#8E8E93'; // Default - Gray - }; - return ( - - - Demo PlugPag Nitro - Terminal: {terminalSerial} - - Status: {isInitialized ? '✅ Pronto' : '❌ Não Inicializado'} - - - - {paymentEvent.code > 0 && ( - - {paymentEvent.message} - Code: {paymentEvent.code} - {paymentEvent.customMessage && ( - {paymentEvent.customMessage} - )} - - )} - - {/* Processing Indicator */} - {isProcessing && ( - - - Processando... - - )} - - + + - Inicializar Terminal - - - - Pagamento Crédito - R$ 25,00 - - - - Pagamento Débito - R$ 15,00 - - - - PIX - R$ 15,00 - - - - - Pagamento Parcelado - R$ 100,00 (3x) - - - - - Estornar Última Transação - - - {lastPayment && ( - - Última Transação - Código: {lastPayment.transactionCode} - ID: {lastPayment.transactionId} - - Status:{' '} - {lastPayment.result === ErrorCode.OK ? '✅ Aprovado' : '❌ Negado'} - - {lastPayment.hostNsu && NSU: {lastPayment.hostNsu}} - {lastPayment.cardBrand && ( - Bandeira: {lastPayment.cardBrand} - )} - - )} - +
+ + + + + + + + + + ); } const styles = StyleSheet.create({ container: { - padding: 20, - backgroundColor: '#f5f5f5', - }, - header: { - alignItems: 'center', - marginBottom: 20, - }, - title: { - fontSize: 24, - fontWeight: 'bold', - color: '#333', - marginBottom: 5, - }, - subtitle: { - fontSize: 16, - color: '#666', - marginBottom: 5, - }, - status: { - fontSize: 14, - fontWeight: '600', - }, - eventContainer: { - padding: 15, - borderRadius: 8, - marginBottom: 15, - }, - eventText: { - color: 'white', - fontWeight: 'bold', - fontSize: 16, - }, - eventCode: { - color: 'white', - fontSize: 12, - marginTop: 5, - }, - eventCustom: { - color: 'white', - fontSize: 14, - marginTop: 5, - fontStyle: 'italic', - }, - processingContainer: { - alignItems: 'center', - padding: 20, - backgroundColor: 'white', - borderRadius: 8, - marginBottom: 15, - }, - processingText: { - marginTop: 10, - fontSize: 16, - color: '#666', - }, - button: { - backgroundColor: '#007AFF', - padding: 15, - borderRadius: 8, - marginBottom: 10, - alignItems: 'center', - }, - primaryButton: { - backgroundColor: '#34C759', - }, - refundButton: { - backgroundColor: '#FF9500', - }, - buttonText: { - color: 'white', - fontSize: 16, - fontWeight: '600', - }, - transactionInfo: { - backgroundColor: 'white', - padding: 15, - borderRadius: 8, - marginTop: 10, + flex: 1, + backgroundColor: theme.colors.background, }, - transactionTitle: { - fontSize: 16, - fontWeight: 'bold', - marginBottom: 5, + contentContainer: { + padding: theme.spacing.md, + paddingBottom: theme.spacing.xl, }, }); diff --git a/example/src/components/Button.tsx b/example/src/components/Button.tsx new file mode 100644 index 0000000..d9bd094 --- /dev/null +++ b/example/src/components/Button.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { + TouchableOpacity, + Text, + StyleSheet, + type TouchableOpacityProps, +} from 'react-native'; +import { theme } from '../constants/theme'; + +export type ButtonVariant = + | 'primary' + | 'secondary' + | 'success' + | 'warning' + | 'error'; + +interface ButtonProps extends TouchableOpacityProps { + title: string; + variant?: ButtonVariant; + fullWidth?: boolean; + loading?: boolean; +} + +export const Button: React.FC = ({ + title, + variant = 'primary', + fullWidth = true, + loading = false, + disabled, + style, + ...props +}) => { + const buttonStyle = [ + styles.button, + styles[variant], + fullWidth && styles.fullWidth, + (disabled || loading) && styles.disabled, + style, + ]; + + const textStyle = [styles.text, (disabled || loading) && styles.disabledText]; + + return ( + + {loading ? 'Processando...' : title} + + ); +}; + +const styles = StyleSheet.create({ + button: { + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: theme.borderRadius.md, + alignItems: 'center', + justifyContent: 'center', + marginBottom: theme.spacing.sm, + ...theme.shadows.small, + }, + fullWidth: { + width: '100%', + }, + text: { + ...theme.typography.body, + fontWeight: '600', + color: theme.colors.textPrimary, + }, + disabled: { + opacity: 0.6, + }, + disabledText: { + color: theme.colors.textSecondary, + }, + + // Variants + primary: { + backgroundColor: theme.colors.primary, + }, + secondary: { + backgroundColor: theme.colors.secondary, + }, + success: { + backgroundColor: theme.colors.success, + }, + warning: { + backgroundColor: theme.colors.warning, + }, + error: { + backgroundColor: theme.colors.error, + }, +}); diff --git a/example/src/components/Card.tsx b/example/src/components/Card.tsx new file mode 100644 index 0000000..739ff2e --- /dev/null +++ b/example/src/components/Card.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { View, StyleSheet, type ViewProps } from 'react-native'; +import { theme } from '../constants/theme'; + +interface CardProps extends ViewProps { + elevated?: boolean; + children: React.ReactNode; +} + +export const Card: React.FC = ({ + elevated = false, + children, + style, + ...props +}) => { + const cardStyle = [styles.card, elevated && styles.elevated, style]; + + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: theme.colors.surface, + borderRadius: theme.borderRadius.md, + padding: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + elevated: { + backgroundColor: theme.colors.surfaceElevated, + ...theme.shadows.medium, + }, +}); diff --git a/example/src/components/Header.tsx b/example/src/components/Header.tsx new file mode 100644 index 0000000..e1cb68a --- /dev/null +++ b/example/src/components/Header.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { theme } from '../constants/theme'; +import { getStatusColor, getStatusText } from '../utils/payment'; +import { Card } from './Card'; + +interface HeaderProps { + terminalSerial: string; + isInitialized: boolean; +} + +export const Header: React.FC = ({ + terminalSerial, + isInitialized, +}) => { + const statusColor = getStatusColor(isInitialized); + const statusText = getStatusText(isInitialized); + + return ( + + + Demo PlugPag Nitro + Terminal: {terminalSerial} + + Status: {statusText} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + }, + title: { + ...theme.typography.h1, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + subtitle: { + ...theme.typography.body, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.xs, + }, + status: { + ...theme.typography.caption, + fontWeight: '600', + }, +}); diff --git a/example/src/components/LoadingIndicator.tsx b/example/src/components/LoadingIndicator.tsx new file mode 100644 index 0000000..42e84d0 --- /dev/null +++ b/example/src/components/LoadingIndicator.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Text, ActivityIndicator, StyleSheet } from 'react-native'; +import { theme } from '../constants/theme'; +import { Card } from './Card'; + +interface LoadingIndicatorProps { + visible: boolean; + message?: string; +} + +export const LoadingIndicator: React.FC = ({ + visible, + message = 'Processando...', +}) => { + if (!visible) return null; + + return ( + + + {message} + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + paddingVertical: theme.spacing.lg, + }, + text: { + ...theme.typography.body, + color: theme.colors.textSecondary, + marginTop: theme.spacing.sm, + }, +}); diff --git a/example/src/components/PaymentButtons.tsx b/example/src/components/PaymentButtons.tsx new file mode 100644 index 0000000..3b5bd43 --- /dev/null +++ b/example/src/components/PaymentButtons.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Button } from './Button'; +import { PAYMENT_BUTTONS } from '../utils/payment'; +import { InstallmentType } from 'react-native-plugpag-nitro'; + +interface PaymentButtonsProps { + isInitialized: boolean; + isProcessing: boolean; + hasLastPayment: boolean; + onInitialize: () => void; + onPayment: (options: { + amount: number; + type: any; + installmentType?: InstallmentType; + installments?: number; + }) => void; + onRefund: () => void; +} + +export const PaymentButtons: React.FC = ({ + isInitialized, + isProcessing, + hasLastPayment, + onInitialize, + onPayment, + onRefund, +}) => { + const handlePaymentClick = ( + buttonConfig: (typeof PAYMENT_BUTTONS)[number] + ) => { + const paymentOptions = { + amount: buttonConfig.amount, + type: buttonConfig.paymentType, + ...('installments' in buttonConfig && { + installmentType: InstallmentType.BUYER_INSTALLMENT, + installments: buttonConfig.installments, + }), + }; + onPayment(paymentOptions); + }; + + return ( + +