Разработка мобильного приложения для сбора пожертвований
Приложения для сбора пожертвований — отдельная категория финтеха с нетривиальными требованиями. Ключевое отличие от обычного интернет-магазина: донаты часто рекуррентные, суммы произвольные (включая «свою сумму»), и пользователь должен видеть, на что именно идут его деньги — иначе доверие падает.
Типы платежей и их реализация
Разовый донат — стандартный платёж через провайдера. Пользователь вводит сумму, нажимает кнопку, видит результат.
Рекуррентный донат — пользователь подписывается на ежемесячное списание. Реализуется через recurring payments: при первом платеже провайдер возвращает токен карты, дальше сервер самостоятельно инициирует списания в нужный день.
// iOS: Stripe recurring donation setup
import StripePayments
// Шаг 1: создать SetupIntent на сервере для сохранения карты без платежа
// Шаг 2: подтвердить с помощью 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 — сохраняем на сервере
self?.saveRecurringMethod(setupIntent?.paymentMethodID)
case .failed:
self?.showError(error?.localizedDescription)
case .canceled:
break
@unknown default: break
}
}
Apple Pay / Google Pay для разовых донатов — максимально низкое трение. Пользователь не вводит данные карты вручную:
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: "Помощь животным",
amount: NSDecimalNumber(string: donationAmount)
)
]
Произвольная сумма: UX-нюансы
Поле «своя сумма» — частый источник ошибок. Проблемы:
- Пользователь вводит «1000.5» или «1 000» с пробелом — нужна нормализация до
Decimal - Минимальный лимит провайдера (у большинства — 10 ₽ или 1 $)
- Максимальный лимит без 3DS
// Android: нормализация произвольной суммы
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("Минимальная сумма — 10 ₽"))
} else if (decimal > BigDecimal("150000")) {
Result.failure(Exception("Для сумм выше 150 000 ₽ требуется верификация"))
} else {
Result.success((decimal * BigDecimal("100")).toLong()) // в копейках
}
} catch (e: NumberFormatException) {
Result.failure(Exception("Введите корректную сумму"))
}
}
Прозрачность: отчёт о использовании средств
Пользователи жертвуют охотнее, когда видят конкретную цель и прогресс. Это «прогресс-бар сбора» — целевой показатель vs текущий.
// 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)} ₽ собрано")
Text("из ${formatAmount(target)} ₽")
}
}
}
Налоговые вычеты и документы
Для благотворительных фондов часто требуется формирование справки для налогового вычета. Это серверная логика: агрегация платежей пользователя за год, формирование PDF. Приложение только показывает кнопку «Скачать справку».
Ориентиры по срокам
Базовая версия (разовые донаты, карта + Apple/Google Pay, история): 3–5 недель. Рекуррентные подписки — ещё 1–2 недели. Прогресс-бары по целевым сборам — ещё 1 неделя. Стоимость рассчитывается индивидуально.







