Overview
The TipModal component provides a user-friendly interface for sending SUI token tips to article authors. It features quick amount selection, custom amount input, and blockchain transaction handling.
Component Interface
interface TipModalProps {
isOpen: boolean
articleId: string
articleTitle: string
onClose: () => void
}
Props
Controls the modal’s visibility state. When true, the modal is displayed and body scroll is prevented.
The unique identifier of the article being tipped. Used to create the blockchain transaction.
The article’s title, displayed in the modal header for context.
Callback function invoked when the modal should close. Called on overlay click, close button, or successful tip.
Usage
Basic Implementation
import { useState } from 'react'
import TipModal from './components/TipModal'
function ArticleCard({ article }) {
const [isTipModalOpen, setIsTipModalOpen] = useState(false)
return (
<>
<button onClick={() => setIsTipModalOpen(true)}>
💰 TIP
</button>
{isTipModalOpen && (
<TipModal
isOpen={isTipModalOpen}
articleId={article.id}
articleTitle={article.title}
onClose={() => setIsTipModalOpen(false)}
/>
)}
</>
)
}
Conditional Rendering
{isTipModalOpen && (
<TipModal
isOpen={isTipModalOpen}
articleId={article.id}
articleTitle={article.title}
onClose={() => setIsTipModalOpen(false)}
/>
)}
The modal should only be rendered when isTipModalOpen is true to prevent unnecessary DOM elements.
Features
Quick Amount Selection
Predefined amounts for fast tipping:
const quickAmounts = [0.01, 0.05, 0.1, 0.5, 1]
Users can click any quick amount button to select it. Active selection is highlighted with the active class.
Numeric input for custom tip amounts:
<input
type="number"
min="0.001"
step="0.001"
value={customAmount}
onChange={(e) => setCustomAmount(e.target.value)}
placeholder="0.000"
/>
Input Constraints:
- Minimum: 0.001 SUI
- Step: 0.001 SUI
- Type: Decimal number
Amount Validation
Validates tip amounts before transaction:
const tipAmount = customAmount ? parseFloat(customAmount) : amount
const amountInMist = suiToMist(tipAmount)
if (!isValidTipAmount(amountInMist)) {
setToast({ message: 'Minimum tip amount is 0.001 SUI', type: 'error' })
return
}
Transaction Flow
Step 1: Amount Selection
- User selects quick amount OR enters custom amount
- UI updates to show selected amount
- Summary displays total amount and estimated gas
Step 2: Transaction Creation
const tx = createTipArticleTransaction(articleId, amountInMist)
Creates a blockchain transaction with:
- Target article ID
- Tip amount in MIST (1 SUI = 1,000,000,000 MIST)
Step 3: Signing & Execution
signAndExecute(
{ transaction: tx },
{
onSuccess: () => {
setToast({ message: `Successfully tipped ${tipAmount} SUI!`, type: 'success' })
setTimeout(() => onClose(), 2000)
},
onError: (error) => {
console.error('Tip failed:', error)
setToast({ message: 'Failed to send tip. Please try again.', type: 'error' })
setIsSubmitting(false)
}
}
)
Step 4: Feedback
- Success: Green toast notification, modal closes after 2s
- Error: Red toast notification, modal stays open
State Management
Internal State
const [amount, setAmount] = useState(0.01)
const [customAmount, setCustomAmount] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
State Reset
State is reset when modal opens:
useEffect(() => {
if (isOpen) {
setAmount(0.01)
setCustomAmount('')
setIsSubmitting(false)
}
}, [isOpen])
UI Elements
Modal Structure
┌─────────────────────────────────────┐
│ 💰 Tip this Article [×] │
├─────────────────────────────────────┤
│ Article Title │
│ │
│ Quick amounts: │
│ [0.01] [0.05] [0.1] [0.5] [1.0] │
│ │
│ Or enter custom amount: │
│ [________] SUI │
│ Minimum: 0.001 SUI │
│ │
│ Tip Amount: 0.01 SUI │
│ Gas Fee: ~0.001 SUI │
├─────────────────────────────────────┤
│ [Cancel] [Send Tip] │
└─────────────────────────────────────┘
Overlay Interaction
Clicking the overlay closes the modal:
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{/* Modal content */}
</div>
</div>
Body Scroll Lock
Prevents background scrolling when modal is open:
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])
Toast Notifications
Integrated Toast component for user feedback:
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
Dependencies
import { useState, useEffect } from 'react'
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit'
import { createTipArticleTransaction, suiToMist, isValidTipAmount } from '../lib/sui'
import Toast from './Toast'
import './TipModal.css'
Required Utilities
createTipArticleTransaction
Creates a blockchain transaction for tipping an article.function createTipArticleTransaction(
articleId: string,
amountInMist: number
): Transaction
Converts SUI tokens to MIST (smallest unit).function suiToMist(sui: number): number
// 1 SUI = 1,000,000,000 MIST
Validates if the tip amount meets minimum requirements.function isValidTipAmount(amountInMist: number): boolean
// Minimum: 0.001 SUI (1,000,000 MIST)
Error Handling
Validation Errors
if (!isValidTipAmount(amountInMist)) {
setToast({ message: 'Minimum tip amount is 0.001 SUI', type: 'error' })
return
}
Transaction Errors
onError: (error) => {
console.error('Tip failed:', error)
setToast({ message: 'Failed to send tip. Please try again.', type: 'error' })
setIsSubmitting(false)
}
Creation Errors
try {
const tx = createTipArticleTransaction(articleId, amountInMist)
// ...
} catch (error) {
console.error('Error creating tip transaction:', error)
setToast({ message: 'Failed to create transaction', type: 'error' })
setIsSubmitting(false)
}
Styling
Modal Classes
.modal-overlay: Full-screen overlay with backdrop
.modal-content: Centered card with brutalist styling
.modal-header: Title and close button
.modal-body: Form content
.modal-footer: Action buttons
<button
disabled={isSubmitting}
className="btn btn-primary"
>
{isSubmitting ? 'Processing...' : 'Send Tip'}
</button>
Best Practices
Always validate amounts before creating transactions to prevent blockchain errors.
// Good: Validate before transaction
if (!isValidTipAmount(amountInMist)) {
showError()
return
}
const tx = createTipArticleTransaction(articleId, amountInMist)
// Bad: Create transaction without validation
const tx = createTipArticleTransaction(articleId, amountInMist)
Reset modal state when opening to prevent stale data from previous interactions.