├── metadata.json ├── .gitignore ├── types.ts ├── index.tsx ├── package.json ├── components ├── FormField.tsx ├── Header.tsx ├── CountdownTimer.tsx ├── icons.tsx └── ImageUploader.tsx ├── vite.config.ts ├── tsconfig.json ├── index.html ├── pages ├── PaymentPage.tsx └── HomePage.tsx ├── README.md └── App.tsx /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Qris Payment App", 3 | "description": "An application to generate a dynamic QRIS payment code and display it in a payment confirmation screen.", 4 | "requestFramePermissions": [] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface DynamicQrisFormData { 2 | paymentAmount: string; 3 | transactionFeeType: 'Persentase' | 'Rupiah'; 4 | transactionFeeValue: string; 5 | } 6 | 7 | export interface PaymentData { 8 | qrString: string; 9 | amount: string; 10 | merchantName: string; 11 | } 12 | 13 | export interface SavedQrisItem { 14 | merchantName: string; 15 | qrisString: string; 16 | } 17 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import App from './App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | if (!rootElement) { 8 | throw new Error("Could not find root element to mount to"); 9 | } 10 | 11 | const root = ReactDOM.createRoot(rootElement); 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qris-payment-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^19.1.0", 13 | "react-dom": "^19.1.0" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^22.14.0", 17 | "typescript": "~5.7.2", 18 | "vite": "^6.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/FormField.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | interface FormFieldProps { 5 | label: string; 6 | htmlFor: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | export const FormField: React.FC = ({ label, htmlFor, children }) => { 11 | return ( 12 |
13 | 16 | {children} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig, loadEnv } from 'vite'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | const env = loadEnv(mode, '.', ''); 6 | return { 7 | define: { 8 | 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), 9 | 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) 10 | }, 11 | resolve: { 12 | alias: { 13 | '@': path.resolve(__dirname, '.'), 14 | } 15 | } 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "allowJs": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | 26 | "paths": { 27 | "@/*" : ["./*"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronLeftIcon, GithubIcon } from './icons'; 3 | 4 | interface HeaderProps { 5 | title: string; 6 | onBack?: () => void; 7 | } 8 | 9 | export const Header: React.FC = ({ title, onBack }) => { 10 | return ( 11 |
12 | {onBack ? ( 13 | 16 | ) : ( 17 |
// Placeholder for spacing 18 | )} 19 |

{title}

20 | {onBack ? ( 21 |
// Placeholder for spacing 22 | ) : ( 23 | 30 | 31 | 32 | )} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | QRIS Dynamic Generator 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /components/CountdownTimer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const COUNTDOWN_SECONDS = 30; 4 | 5 | export const CountdownTimer: React.FC = () => { 6 | const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS); 7 | 8 | useEffect(() => { 9 | if (countdown <= 0) return; 10 | 11 | const intervalId = setInterval(() => { 12 | setCountdown(prev => prev - 1); 13 | }, 1000); 14 | 15 | return () => clearInterval(intervalId); 16 | }, [countdown]); 17 | 18 | const radius = 28; 19 | const circumference = 2 * Math.PI * radius; 20 | const progress = countdown / COUNTDOWN_SECONDS; 21 | const strokeDashoffset = circumference * (1 - progress); 22 | 23 | return ( 24 |
25 | 26 | {/* Background circle */} 27 | 35 | {/* Progress circle */} 36 | 49 | 50 | 51 | {countdown} 52 | 53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IconProps { 4 | className?: string; 5 | } 6 | 7 | export const ChevronLeftIcon: React.FC = ({ className }) => ( 8 | 9 | ); 10 | 11 | export const MoreHorizontalIcon: React.FC = ({ className }) => ( 12 | 13 | ); 14 | 15 | export const ImagePlusIcon: React.FC = ({ className }) => ( 16 | 17 | ); 18 | 19 | export const ChevronDownIcon: React.FC = ({ className }) => ( 20 | 21 | ); 22 | 23 | export const TrashIcon: React.FC = ({ className }) => ( 24 | 25 | ); 26 | 27 | export const QrisLogoIcon: React.FC = ({ className }) => ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | 37 | export const GithubIcon: React.FC = ({ className }) => ( 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /pages/PaymentPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import type { PaymentData } from '../types'; 3 | import { Header } from '../components/Header'; 4 | 5 | // Make qrcode available from the global scope (window) 6 | declare var qrcode: any; 7 | 8 | interface PaymentPageProps { 9 | paymentData: PaymentData; 10 | onBack: () => void; 11 | } 12 | 13 | const formatCurrency = (amount: string) => { 14 | const number = parseFloat(amount); 15 | if (isNaN(number)) return "0.00"; 16 | return new Intl.NumberFormat('id-ID', { 17 | minimumFractionDigits: 2, 18 | maximumFractionDigits: 2 19 | }).format(number); 20 | }; 21 | 22 | export const PaymentPage: React.FC = ({ paymentData, onBack }) => { 23 | const qrCodeRef = useRef(null); 24 | 25 | useEffect(() => { 26 | if (paymentData.qrString && qrCodeRef.current) { 27 | qrCodeRef.current.innerHTML = ''; // Clear previous QR code 28 | try { 29 | const qr = qrcode(0, 'M'); // type 0, error correction 'M' 30 | qr.addData(paymentData.qrString); 31 | qr.make(); 32 | // Use a larger cell size for a higher-res base image, looks better when scaled. 33 | qrCodeRef.current.innerHTML = qr.createImgTag(8, 4); // (module size, margin) 34 | // Make the injected img responsive 35 | const img = qrCodeRef.current.querySelector('img'); 36 | if (img) { 37 | img.style.width = '100%'; 38 | img.style.height = 'auto'; 39 | img.style.imageRendering = 'pixelated'; // Keep pixels sharp 40 | } 41 | } catch (e) { 42 | console.error("Failed to render QR Code image:", e); 43 | } 44 | } 45 | }, [paymentData.qrString]); 46 | 47 | return ( 48 | <> 49 |
50 |
51 |

{paymentData.merchantName}

52 |

53 | Payment of Rp{formatCurrency(paymentData.amount)} 54 |

55 | 56 |
57 |
58 | {/* QR Code is injected here */} 59 |
60 |
61 | 62 |

63 | E-Wallet transaction cannot be refunded 64 |

65 |
66 | 67 | ); 68 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QRIS Dynamic Generator 2 | 3 | This is a web-based tool to convert a static QRIS (Quick Response Code Indonesian Standard) into a dynamic one. Users can upload a static QRIS image, specify a payment amount and an optional transaction fee, and the application will generate a new, dynamic QRIS code ready for payment. The app also conveniently saves your previously used QRIS codes for quick access. 4 | 5 | ## ✨ Features 6 | 7 | * **Static to Dynamic QRIS Conversion**: The core feature is to generate a dynamic QRIS from a static one by embedding the payment amount. 8 | * **QR Code Scanning**: Upload a QRIS code image (PNG, JPG, GIF) to automatically scan its data. 9 | * **Custom Amount & Fee**: Set a specific payment amount for the dynamic QRIS. 10 | * **Flexible Transaction Fees**: Add a transaction fee, calculated either as a fixed amount (Rupiah) or a percentage. 11 | * **QRIS History**: The app saves up to 5 of your most recently used QRIS codes in your browser's local storage for easy reuse. 12 | * **Payment-Ready Display**: Shows the generated dynamic QRIS on a clean payment screen, complete with the merchant's name and the total amount. 13 | * **Responsive Design**: A mobile-first design that works smoothly on both desktop and mobile browsers. 14 | 15 | ## 🚀 How It Works 16 | 17 | 1. **Upload & Scan**: The user uploads an image of a static QRIS code. The `jsQR` library is used to read the QR code from the image and extract the static QRIS string data. 18 | 2. **Input Data**: The user enters the desired payment amount and, optionally, a transaction fee on the main form. 19 | 3. **Generate Dynamic QRIS**: The application takes the static QRIS data and injects new tags for the payment amount (`54`) and transaction fee (`55`, `56`, or `57`). It then replaces the static QRIS indicator (`010211`) with the dynamic one (`010212`). 20 | 4. **Recalculate CRC**: A new CRC-16 checksum is calculated for the modified QRIS string to ensure its validity. 21 | 5. **Render QR Code**: The final dynamic QRIS string is rendered as a new QR code image on the payment page using the `qrcode-generator` library. 22 | 23 | ## 🛠️ Technologies Used 24 | 25 | * **Frontend**: React 19, TypeScript 26 | * **Build Tool**: Vite 27 | * **Styling**: Tailwind CSS 28 | * **QR Code Reading**: `jsQR` 29 | * **QR Code Generation**: `qrcode-generator` 30 | * **Icons**: Font Awesome 31 | 32 | ## ⚙️ Setup and Run Locally 33 | 34 | **Prerequisites:** [Node.js](https://nodejs.org/) 35 | 36 | 1. **Clone the repository:** 37 | ```bash 38 | git clone https://github.com/your-username/qris-dinamis-generator.git 39 | cd qris-dinamis-generator 40 | ``` 41 | 42 | 2. **Install dependencies:** 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | 48 | 3. **Run the development server:** 49 | ```bash 50 | npm run dev 51 | ``` 52 | 53 | The application will be running at `http://localhost:5173` (or another port if 5173 is busy). 54 | -------------------------------------------------------------------------------- /components/ImageUploader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useCallback } from 'react'; 2 | import { ImagePlusIcon } from './icons'; 3 | 4 | // Make jsQR and qrcode available from the global scope (window) 5 | declare var jsQR: any; 6 | 7 | interface ImageUploaderProps { 8 | onQrDecode: (data: string | null, error?: string) => void; 9 | } 10 | 11 | export const ImageUploader: React.FC = ({ onQrDecode }) => { 12 | const [imagePreview, setImagePreview] = useState(null); 13 | const [scanStatus, setScanStatus] = useState<'idle' | 'success' | 'error'>('idle'); 14 | const fileInputRef = useRef(null); 15 | const canvasRef = useRef(null); 16 | 17 | const handleFileChange = (event: React.ChangeEvent) => { 18 | const file = event.target.files?.[0]; 19 | if (!file) { 20 | setImagePreview(null); 21 | setScanStatus('idle'); 22 | onQrDecode(null); 23 | return; 24 | } 25 | 26 | const reader = new FileReader(); 27 | reader.onloadend = () => { 28 | const result = reader.result as string; 29 | setImagePreview(result); 30 | 31 | const image = new Image(); 32 | image.onload = () => { 33 | const canvas = canvasRef.current; 34 | if (canvas) { 35 | const context = canvas.getContext('2d', { willReadFrequently: true }); 36 | if (context) { 37 | canvas.width = image.width; 38 | canvas.height = image.height; 39 | context.drawImage(image, 0, 0, image.width, image.height); 40 | const imageData = context.getImageData(0, 0, image.width, image.height); 41 | const code = jsQR(imageData.data, imageData.width, imageData.height, { 42 | inversionAttempts: "dontInvert", 43 | }); 44 | 45 | if (code) { 46 | setScanStatus('success'); 47 | onQrDecode(code.data); 48 | } else { 49 | setScanStatus('error'); 50 | onQrDecode(null, 'Could not find a QR code in the image.'); 51 | } 52 | } 53 | } 54 | }; 55 | image.src = result; 56 | }; 57 | reader.readAsDataURL(file); 58 | }; 59 | 60 | const handleClick = useCallback(() => { 61 | fileInputRef.current?.click(); 62 | }, []); 63 | 64 | const statusClasses = { 65 | idle: 'border-zinc-300 hover:border-blue-500', 66 | success: 'border-green-500', 67 | error: 'border-red-500', 68 | } 69 | 70 | return ( 71 | <> 72 |
76 | 83 | {imagePreview ? ( 84 | QRIS preview 85 | ) : ( 86 |
87 | 88 | Upload QRIS 89 |
90 | )} 91 |
92 | 93 | 94 | ); 95 | }; -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { HomePage } from './pages/HomePage'; 3 | import { PaymentPage } from './pages/PaymentPage'; 4 | import type { PaymentData } from './types'; 5 | 6 | // Make qrcode available from the global scope (window) 7 | declare var qrcode: any; 8 | 9 | // Utility function to calculate CRC16 checksum, ported from the PHP example 10 | const crc16 = (str: string): string => { 11 | let crc = 0xFFFF; 12 | const strlen = str.length; 13 | for (let c = 0; c < strlen; c++) { 14 | crc ^= str.charCodeAt(c) << 8; 15 | for (let i = 0; i < 8; i++) { 16 | if (crc & 0x8000) { 17 | crc = (crc << 1) ^ 0x1021; 18 | } else { 19 | crc = crc << 1; 20 | } 21 | } 22 | } 23 | const hex = (crc & 0xFFFF).toString(16).toUpperCase(); 24 | return hex.padStart(4, '0'); 25 | }; 26 | 27 | // Extracts the merchant name (Tag 59) from the static QRIS string. 28 | const parseMerchantName = (qrisData: string): string => { 29 | const tag = '59'; 30 | const tagIndex = qrisData.indexOf(tag); 31 | if (tagIndex === -1) { 32 | return 'Merchant'; // Default name if tag not found 33 | } 34 | 35 | try { 36 | const lengthIndex = tagIndex + tag.length; 37 | const lengthStr = qrisData.substring(lengthIndex, lengthIndex + 2); 38 | const length = parseInt(lengthStr, 10); 39 | 40 | if (isNaN(length) || length <= 0) { 41 | return 'Merchant'; 42 | } 43 | 44 | const valueIndex = lengthIndex + 2; 45 | const merchantName = qrisData.substring(valueIndex, valueIndex + length); 46 | return merchantName.trim() || 'Merchant'; 47 | } catch (e) { 48 | console.error("Failed to parse merchant name:", e); 49 | return 'Merchant'; 50 | } 51 | }; 52 | 53 | // Main logic to generate dynamic QRIS string, ported from PHP 54 | const generateDynamicQris = ( 55 | staticQris: string, 56 | amount: string, 57 | feeType: 'Persentase' | 'Rupiah', 58 | feeValue: string 59 | ): string => { 60 | if (staticQris.length < 4) { 61 | throw new Error('Invalid static QRIS data.'); 62 | } 63 | 64 | const qrisWithoutCrc = staticQris.substring(0, staticQris.length - 4); 65 | const step1 = qrisWithoutCrc.replace("010211", "010212"); 66 | 67 | const parts = step1.split("5802ID"); 68 | if (parts.length !== 2) { 69 | throw new Error("QRIS data is not in the expected format (missing '5802ID')."); 70 | } 71 | 72 | const amountStr = String(parseInt(amount, 10)); // Remove leading zeros and decimals 73 | const amountTag = "54" + String(amountStr.length).padStart(2, '0') + amountStr; 74 | 75 | let feeTag = ""; 76 | if (feeValue && parseFloat(feeValue) > 0) { 77 | if (feeType === 'Rupiah') { 78 | const feeValueStr = String(parseInt(feeValue, 10)); 79 | feeTag = "55020256" + String(feeValueStr.length).padStart(2, '0') + feeValueStr; 80 | } else { // Persentase 81 | const feeValueStr = feeValue; 82 | feeTag = "55020357" + String(feeValueStr.length).padStart(2, '0') + feeValueStr; 83 | } 84 | } 85 | 86 | const payload = [parts[0], amountTag, feeTag, "5802ID", parts[1]].join(''); 87 | 88 | const finalCrc = crc16(payload); 89 | return payload + finalCrc; 90 | }; 91 | 92 | 93 | const App: React.FC = () => { 94 | const [route, setRoute] = useState<'home' | 'payment'>('home'); 95 | const [paymentData, setPaymentData] = useState(null); 96 | 97 | const handleGenerate = ( 98 | staticQris: string, 99 | amount: string, 100 | feeType: 'Persentase' | 'Rupiah', 101 | feeValue: string 102 | ) => { 103 | const qrString = generateDynamicQris(staticQris, amount, feeType, feeValue); 104 | const merchantName = parseMerchantName(staticQris); 105 | setPaymentData({ qrString, amount, merchantName }); 106 | setRoute('payment'); 107 | }; 108 | 109 | const handleBackToHome = () => { 110 | setPaymentData(null); 111 | setRoute('home'); 112 | }; 113 | 114 | return ( 115 |
116 |
117 |
118 | {route === 'home' && } 119 | {route === 'payment' && paymentData && } 120 |
121 |
122 | Code by Ogya Adyatma Putra & cetass cetass 123 |
124 |
125 |
126 | ); 127 | }; 128 | 129 | export default App; -------------------------------------------------------------------------------- /pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Header } from '../components/Header'; 3 | import { FormField } from '../components/FormField'; 4 | import { ImageUploader } from '../components/ImageUploader'; 5 | import { ChevronDownIcon, TrashIcon } from '../components/icons'; 6 | import type { DynamicQrisFormData, SavedQrisItem } from '../types'; 7 | 8 | const MAX_SAVED_QRIS = 5; 9 | 10 | interface HomePageProps { 11 | onGenerate: ( 12 | staticQris: string, 13 | amount: string, 14 | feeType: 'Persentase' | 'Rupiah', 15 | feeValue: string 16 | ) => void; 17 | parseMerchantName: (qrisData: string) => string; 18 | } 19 | 20 | export const HomePage: React.FC = ({ onGenerate, parseMerchantName }) => { 21 | const [formData, setFormData] = useState({ 22 | paymentAmount: '', 23 | transactionFeeType: 'Persentase', 24 | transactionFeeValue: '', 25 | }); 26 | const [staticQrisData, setStaticQrisData] = useState(null); 27 | const [error, setError] = useState(null); 28 | const [savedQris, setSavedQris] = useState([]); 29 | 30 | useEffect(() => { 31 | try { 32 | const storedQris = localStorage.getItem('qrisHistory'); 33 | if (storedQris) { 34 | setSavedQris(JSON.parse(storedQris)); 35 | } 36 | } catch (e) { 37 | console.error("Failed to load QRIS history from localStorage", e); 38 | localStorage.removeItem('qrisHistory'); 39 | } 40 | }, []); 41 | 42 | const updateSavedQris = (newHistory: SavedQrisItem[]) => { 43 | setSavedQris(newHistory); 44 | localStorage.setItem('qrisHistory', JSON.stringify(newHistory)); 45 | }; 46 | 47 | const handleQrDecode = (data: string | null, errorMessage?: string) => { 48 | setStaticQrisData(data); 49 | if (errorMessage && !data) { 50 | setError(errorMessage); 51 | } else { 52 | setError(null); 53 | } 54 | 55 | if (data) { 56 | const isDuplicate = savedQris.some(item => item.qrisString === data); 57 | if (!isDuplicate) { 58 | const merchantName = parseMerchantName(data); 59 | const newItem: SavedQrisItem = { merchantName, qrisString: data }; 60 | 61 | let newHistory = [newItem, ...savedQris]; 62 | if (newHistory.length > MAX_SAVED_QRIS) { 63 | newHistory = newHistory.slice(0, MAX_SAVED_QRIS); 64 | } 65 | updateSavedQris(newHistory); 66 | } 67 | } 68 | }; 69 | 70 | const handleChange = (e: React.ChangeEvent) => { 71 | const { name, value } = e.target; 72 | setFormData((prev) => ({ ...prev, [name]: value })); 73 | }; 74 | 75 | const handleUseQris = (qrisString: string) => { 76 | setStaticQrisData(qrisString); 77 | setError(null); 78 | }; 79 | 80 | const handleDeleteQris = (qrisStringToDelete: string) => { 81 | const newHistory = savedQris.filter(item => item.qrisString !== qrisStringToDelete); 82 | updateSavedQris(newHistory); 83 | if (staticQrisData === qrisStringToDelete) { 84 | setStaticQrisData(null); 85 | } 86 | }; 87 | 88 | const handleSubmit = (e: React.FormEvent) => { 89 | e.preventDefault(); 90 | setError(null); 91 | 92 | if (!staticQrisData) { 93 | setError("Please upload or select a valid static QRIS code first."); 94 | return; 95 | } 96 | if (!formData.paymentAmount || parseFloat(formData.paymentAmount) <= 0) { 97 | setError("Please enter a valid payment amount."); 98 | return; 99 | } 100 | 101 | try { 102 | onGenerate( 103 | staticQrisData, 104 | formData.paymentAmount, 105 | formData.transactionFeeType, 106 | formData.transactionFeeValue 107 | ); 108 | } catch (err: any) { 109 | setError(err.message || 'Failed to generate QRIS code. Please check the inputs.'); 110 | } 111 | }; 112 | 113 | return ( 114 | <> 115 |
116 | 117 |
118 | 119 | 120 | {error &&
{error}
} 121 | {staticQrisData && !error &&
QRIS Scanned: {staticQrisData}
} 122 | 123 | {savedQris.length > 0 && ( 124 |
125 |

Saved QRIS

126 |
    127 | {savedQris.map((item) => ( 128 |
  • 136 | {item.merchantName} 137 |
    138 | 145 | 153 |
    154 |
  • 155 | ))} 156 |
157 |
158 | )} 159 | 160 | 161 |
162 | Rp 163 | 174 |
175 |
176 | 177 | 178 |
179 |
180 | 190 | 191 |
192 |
193 | 204 | 205 | {formData.transactionFeeType === 'Persentase' ? '%' : 'Rp'} 206 | 207 |
208 |
209 |
210 | 211 |
212 | 215 |
216 | 217 | 218 | ); 219 | }; --------------------------------------------------------------------------------