Developing a Mobile App for Donation Collection
Donation collection apps—separate fintech category with non-trivial requirements. Key difference from standard eCommerce: donations often recurring, amounts arbitrary (including "custom"), user must see exactly where money goes—otherwise trust drops.
Payment Types and Implementation
One-time donation—standard payment via provider. User enters sum, clicks button, sees result.
Recurring donation—user subscribes to monthly deduction. Implemented via recurring payments: on first payment, provider returns card token, server then initiates deductions on set day.
// iOS: Stripe recurring donation setup
import StripePayments
// Step 1: create SetupIntent on server for card save without payment
// Step 2: confirm with STPPaymentHandler
let params = STPConfirmSetupIntentParams(
paymentMethodParams: cardParams,
clientSecret: setupIntentClientSecret
)
STPPaymentHandler.shared().confirmSetupIntent(
params,
with: self
) { [weak self] status, setupIntent, error in
switch status {
case .succeeded:
// setupIntent.paymentMethodID—save on server
self?.saveRecurringMethod(setupIntent?.paymentMethodID)
case .failed:
self?.showError(error?.localizedDescription)
case .canceled:
break
@unknown default: break
}
}
Apple Pay / Google Pay for one-time donations—lowest friction. User doesn't enter card details manually:
let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.com.yourcharity.app"
request.countryCode = "RU"
request.currencyCode = "RUB"
request.supportedNetworks = [.visa, .masterCard]
request.merchantCapabilities = [.capability3DS]
request.paymentSummaryItems = [
PKPaymentSummaryItem(
label: "Help Animals",
amount: NSDecimalNumber(string: donationAmount)
)
]
Custom Amount: UX Nuances
"Custom amount" field—frequent error source. Issues:
- User enters "1000.5" or "1 000" with space—normalize to
Decimal - Provider minimum limit (most—10 ₽ or 1 $)
- Max limit without 3DS
// Android: custom amount normalization
fun parseAmount(input: String): Result<Long> {
val cleaned = input
.replace(",", ".")
.replace(Regex("\\s"), "")
.trim()
return try {
val decimal = cleaned.toBigDecimal()
if (decimal < BigDecimal("10")) {
Result.failure(Exception("Minimum—10 ₽"))
} else if (decimal > BigDecimal("150000")) {
Result.failure(Exception("Sums over 150,000 ₽ require verification"))
} else {
Result.success((decimal * BigDecimal("100")).toLong()) // kopecks
}
} catch (e: NumberFormatException) {
Result.failure(Exception("Enter valid amount"))
}
}
Transparency: Fund Usage Report
Users donate more when seeing concrete goal and progress. "Fundraising progress bar"—target vs current.
// Android: Jetpack Compose progress indicator
@Composable
fun FundraisingProgress(
current: Long,
target: Long,
modifier: Modifier = Modifier
) {
val progress = (current.toFloat() / target.toFloat()).coerceIn(0f, 1f)
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 800)
)
Column(modifier) {
LinearProgressIndicator(
progress = animatedProgress,
modifier = Modifier.fillMaxWidth().height(8.dp),
trackColor = MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.primary
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("${formatAmount(current)} ₽ raised")
Text("of ${formatAmount(target)} ₽")
}
}
}
Tax Deductions and Documents
For charities often need tax deduction form generation. Server logic: aggregate user payments for year, generate PDF. App just shows "Download Receipt" button.
Timeline Estimates
Basic version (one-time donations, card + Apple/Google Pay, history): 3–5 weeks. Recurring subscriptions—another 1–2 weeks. Fundraising progress bars—another 1 week. Pricing is calculated individually.







