Як я став першим, хто підключив LiqPay до застосунку на Flutter

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Усім привіт, це Олег Новосад, я IT-архітектор, викладач, а тут я виступаю ще й як автор гейміфікованого мобільного застосунку «Давай займемось текстом», який ми робимо для того, щоб українською мовою охоче почали говорити щонайменше 40 мільйонів людей.

Перший реліз відбувся 25 листопада 2022 — і з того часу вдалося досягти непоганих результатів: ми в десятці найкращих застосунків України на App Store, побували на першому місці в категорії «Освіта», перемогли в конкурсі стартапів та здобули нагороду за соціальний внесок.

Також завдяки нашій дещо нестандартній моделі монетизації застосунок водночас доступний абсолютно для всіх користувачів незалежно від рівня їхніх доходів, а також заробляє сам на себе. Чому і як я підключав платіжну систему LiqPay до Flutter, а також про те, що з цього вийшло, ви дізнаєтеся з цієї статті.

Модель монетизації

Призначення застосунку в нас незвичне: він закохує в українську мову. Іншими словами, він:
  • руйнує стереотипи щодо мови в зросійщених українців та спонукає їх переходити на українську;
  • відкриває очі українськомовним українцям на те, скільки мовних скарбів у нас є і як ними можна користуватися собі на радість та користь.

Отже, ми робимо його для всіх мільйонів дорослих українців, що живуть вдома або за кордоном. Як бачите, у нашого стартапу не тільки бізнесові цілі, але ще й соціальні.

До того ж ми звичайні люди, застосунок — це наша ініціатива, нас ніхто не фінансує. Тому ми від самого початку виходили з того, що він мусить, по-перше, бути доступним для всіх, по-друге, окупити витрати на розробку, підтримку та розвиток.

Після міркувань, ми сформували модель монетизації наближену до Freemium, але з певними відмінностями. Дохід у нас формується з чотирьох джерел:

  1. Реклама.
  2. Підписки, що позбавляють від реклами та додають комфорту користування.
  3. Внутрішні покупки (ігрова валюта та додатковий контент).
  4. Донейти.

Ми з співавторкою застосунку Наталею Місюк маємо домовленість про те, доходи з яких джерел покривають які саме статті розходів, тож нам було потрібно, щоб донейти та гроші від додаткового контенту потрапляли на один рахунок, а гроші від реклами та підписок — на інший. Ми почали шукати платіжну систему, яка була б зручною як для користувачів, так і дозволяла б реалізувати все те, що ми придумали.

До речі, чому застосунок саме на Flutter, я розповідав ось тут.

Вибір платіжної системи

Задача підібрати підхожу платіжну систему виявилася не дуже простою. Ми розглядали Stripe, але він не працював з Україною, тож ми від нього відмовилися. Був варіант підключити portmone, але в Наталі з ними неприємно склалося спілкування: вони не вміли працювати з донейтами, а також наговорили зайвого. Хоча вони потім були змушені вибачитись, неприємний осад усе ж залишився, і працювати з ними ми не хотіли.

У поле зору потрапили також Fondy та LiqPay. Перший Наталя обрала, щоби підключити донейти до сайту на Wix. Було не дуже зрозуміло, чи вдасться застосувати для застосунку теж. А от LiqPay на перший погляд обіцяв легку інтеграцію, хорошу службу підтримки та широкий спектр варіантів оплати. Також LiqPay добре знайомий користувачам, бо вони часто його бачать на різних сайтах, тож знають його інтерфейс та довіряють йому.

Були ще й інші варіанти, але вони значно поступалися привабливістю LiqPay. Принаймні, на той час та за тими даними, що були в нашому розпорядженні.

Варіант підключення перший

Перший варіант підключення, про який я подумав та побачив за їхніми відкритими API, це оплата через введення даних картки. Тобто коли користувач хотів би придбати навчальний курс, йому потрібно було б ввести номер картки, ім’я, термін дії, cvv-код. Звучало непогано.

LiqPay надавав тестові картки для перевірки. Я спробував підключити такий варіант — і ніби вийшло. Я робив запит на оплату, мені приходила відповідь — усе куплено, усе працює, усе добре. Настільки добре, що аж підозріло. Я до того вже стикався з платіжними системами, щоправда з іншими, і знав, що тут часто виникають складнощі, тому мені не вірилось, що все дійсно може бути так легко.

Я прочитав купу статей, блогів, форумів про те, як зробити оплату за принципом наглого введення даних картки, та був уже навіть почав писати користувацький інтерфейс, для того щоб користувачу не треба було щоразу заповнювати поля, а дані якимось чином зберігалися. Також знайшов у LiqPay можливість авторизувати певну картку з отриманням спеціального токену для подальших покупок та потім побачив, як дрібненьким шрифтом на сайті LiqPay було написано, що для такого типу інтеграції мені треба мати спеціальні сертифікації, підтвердження і все в такому дусі.

І тут я зрозумів, що в нашому випадку це шлях у нікуди. Якщо я почну займатися питаннями сертифікацій, є ризик, що реліз застосунку сильно затягнеться і взагалі навряд чи колись відбудеться. Тож від такого способу підключення платіжної системи довелося відмовитися. Проте, я наводжу тут шматочок коду, щоби проілюструвати саму можливість інтеграції такого типу. Якщо хтось із вас уже має необхідні підтвердження, то такий варіант може стати в пригоді:

final liqPay = LiqPay("public_liqpay_key", "private_liqpay_key");
final number = "4242424242424242";
final expirationMonth = "12";
final expirationYear = "99";
final cvv = "000";
final card = LiqPayCard(number, expirationMonth, expirationDate, cvv);
final order = LiqPayOrder(const Uuid().v4(), 1, 'Test', card: card, action: LiqPayAction.pay);

await liqPay.purchase(order);

Варіант підключення другий

LiqPay популярний. Якщо ви хочете щось замовити в інтернеті — їжу в ресторані, доставку питної води, ще щось, — то дуже ймовірно, що в багатьох випадках оплата буде проходити саме через нього. Фактично, користувач заходить на сайт, звідки потрапляє у віконечко LiqPay — безпечне середовище платіжної системи — де вводить дані картки. Це звично, у людей не виникає питань «хто це тут просить дані моєї картки?», усе добре та надійно.

Крім того, ще додається можливість оплати через Apple Pay або Google Pay чи Privat24, що дуже зручно й чого не зробиш у першому варіанті підключення, про який я розповів вище.

Як реалізувати підключення через таке віконечко? У принципі, технічно це не дуже складно. Всередині мобільного застосунку можна відкривати вебсторінки — і, власне, саме це ми й робимо. Щоразу, коли користувач у нас хоче придбати курс або зробити донейт, він бачить насправді вебсторінку LiqPay і все, що стосується оплати, відбувається в ній.

Щоправда, є тонкий момент: крім проведення безпосередньо оплати, ще потрібно якимось чином передати додаткові дані: який саме курс було куплено, яким був розмір донату, чи користувач написав щось у коментарі до нього тощо.

Усе це критично важливо як для обліку та аналітики, так і для того, щоб усе коректно працювало: куплені курси ставали доступними для користувачів, а якщо оплата не пройшла з якоїсь причини, то не діставалися користувачам на халяву. Як це реалізувати?

Робота напряму означала б написання дуже великої кількості коду, тому я шукав бібліотеки для роботи з LiqPay — і не знаходив. Тоді я вирішив створити свою, яку й застосував для застосунку. Вона вже знаходиться у публічному доступі на pub.dev, вона open-source, тому при бажанні ви також можете долучитись та покращувати її, і це поки що перша та єдина така на Dart/ Flutter.

final order = LiqPayOrder(
  const Uuid().v4(),
  amount,
  commentController.text.isEmpty ? "Донат" : commentController.text,
  serverUrl: '$serverUrl/OnDonate?user_id=${user?.id}',
  currency: LiqPayCurrency.uah,
  language: LiqPayLanguage.uk);
final url = await getIt<LiqPay>().checkout(order);
if (mounted) {
  Navigator.of(context).push(
    MaterialPageRoute(
      builder: (context) => BrowserScreen(url: url, title: "Донат")
    )
  );
}

Отже, я знайшов та запровадив рішення — і це добре. Проте, тішитися було рано: з досвіду роботи я знав, що Apple в принципі не любить, коли застосунки запускають браузери. Це проблема, адже деякі частини застосунку (наприклад, Політика конфіденційності, Правила використання тощо) у нас відкриваються саме в такий спосіб. Чи дозволить Apple оплату через віконечко LiqPay, я не був впевнений. Але дізнатися напевно можна було тільки якщо спробувати.

Почався процес інтеграції. Служба підтримки LiqPay не могла дати чіткої відповіді, яким чином і куди я маю отримати відповідь про те, що оплата пройшла. Чи ця відповідь мала приходити в сам застосунок, наприклад, callback`ом, чи кудись у небо, де я мав її впіймати — не вдавалося з’ясувати.

Мені довелося далі копирсатися в документації, де в результаті я і знайшов, знову дрібненьким шрифом, що мені потрібно самому вказати посиланням, куди я хочу отримати відповідь. Іншими словами, це мав бути якийсь сервер, куди сервер LiqPay буде надсилати запит «ось, тобі прийшла оплата». Далі в документації довго описувалися система кодування відповіді та запиту.

Мені довелося написати Firebase-функції, які в нас використовуються для курсів та донатів. Нижче наведено приклад такої для донатів:

/**
 * Function to react on donate.
 * Should be triggered by LiqPay server with purchase result.
 *
 * Query param should contain `user_id`.
 *
 * @param {functions.https.Request} request
 * @param {express.Response} response
 */
export async function onDonate(
  request: functions.https.Request,
  response: express.Response
) {
  const data = request.body["data"];
  const signature = request.body["signature"];
  
  functions.logger.debug(`Data: ${data}`);
  functions.logger.debug(`Signature: ${signature}`);
  const verified = Verifier.verifySignature(data, signature);
  
  if (!verified) {
    functions.logger.error("Signatures do not match.");
    response.status(403).send("Forbidden");
    return;
  }
  
  const userId = request.query["user_id"] as string;
  functions.logger.info(`User id: ${userId}`);
  const userRef = admin.firestore().collection("users").doc(userId);
  const user = (await userRef.get()).data() as User;
 
  const jsonString = Buffer.from(data, "base64").toString();
  functions.logger.debug(`Data JSON: ${jsonString}`);
  const json = JSON.parse(jsonString);
  
  let notificationData;
  if (json["status"] == "success") {
    user.stats.donates += 1;
    await userRef.update({"stats.donates": user.stats.donates});
    notificationData = {
      result: "success",
    };
  } else {
    notificationData = {
      result: "error",
      errorCode: json["err_code"],
      errorDescription: json["err_description"],
    };
  }
  
  const payload: MessagingPayload = {
    notification: notificationData.result == "success" ? {
      title: "Дякуємо за донат!",
      body: "Ви допомагаєте нам ставати кращими! 💛",
    } : {
      title: "Щось сі всрало :(",
      body: "Ваш донат не пройшов успішно. Ми вже працюємо над цим! Дякуємо за розуміння! 💛",
    },
    data: {
      type: NotificationType.DONATE,
      data: JSON.stringify(notificationData),
    },
  };
  
  await Sender.sendSinglePushNotification(payload, user);
  
  response.status(200).send("OK");
  return;
}

У випадку з курсами, вони стають доступними користувачу, коли і якщо оплата успішно пройшла. Якщо ж на карті, скажімо, бракує коштів або збіг термін її дії, то оплата не проходить, відповідно, курс залишається закритим.

Я написав цей серверний код, оплати почали працювати, інтеграція пройшла нібито успішно. З екрану донатів ми передавали розмір донату, коментар, усе це запаковувалося за допомогою бібліотеки в запит на LiqPay.

Після оплати LiqPay присилає підтвердження на цей сервер. Тобто все готово, усе працює, усе класно. Ми всією командою ретельно це протестували та заспамили ФОП Наталі пів сотнею оплат по 1 гривні: таку ціну для курсів я поставив для зручності на етапі тестування, тому що оплату в нуль гривень LiqPay встановити не дозволяв. Хоча все вийшло добре, радіти я все ще не поспішав, адже попереду на нас чекала перевірка від Apple.

Складнощі розгляду

Google в плані перевірки застосунків набагато менш прискіпливий, ніж Apple. Він спокійно дозволяє використання браузерів, тому з проходженням перевірки на Google Play не виникло проблем. Усе відбулося легко та швидко.

А ось Apple не був до нас таким добрим: хоча вони не були проти оплати курсів через LiqPay, та сказали нам, що донати так робити ніяк не можна, натомість запропонували скористатися їхньої системою та провести грошові подарунки як upsales всередині застосунку. З’ясування цього в нас зайняло цілий місяць часу, про це детальніше ми розповідали тут.

Я почав дивитися, як зробити донати в Apple, тому що ніколи раніше мені не доводилося про таке чути. Виявилося, що донати як віконечко, у яке користувач може ввести будь-яку суму на власний розсуд, можна було зробити тільки для благодійних організацій. Оскільки в нас йшлося про ФОП, то Apple залишав можливість оформити донейти як внутрішні покупки фіксованих розмірів у доларах.

Оскільки дата релізу вже була затягнута так, що далі нікуди, мені довелося на шаленій швидкості переробляти віконечко та закидати застосунок на повторний розгляд. І він пройшов успішно! Ми змогли запустити застосунок!

До речі, передбачаю ваше питання: так, за українським законодавством ФОПи на третій групі можуть отримувати гроші в подарунок, чим фактично і є донати. З них платиться податок. Це, якщо дуже коротко, щодо деталей раджу консультуватись із юристами та бухгалтерами, якщо хочете запровадити таке й собі.

Висновки

Цікаво, що я раніше ніде не бачив і не чув, не знайшов в інтернеті чужого досвіду з інтеграції Flutter та LiqPay, так що я можу з упевненістю сказати, що зробив це першим.

Також хочу відзначити, що документація LiqPay хоч і детальна та якісно зроблена, проте питання з інтеграцією виявилося для служби підтримки задуже складним. У мене склалося враження, що вони навіть не розуміли суть мого запиту, відповідно, нічого толком не могли підказати. У найкращому випадку просто скеровували мене читати документацію.

У попередніх публікаціях я вже писав, що ідея застосунку в нас цікава: спонукати зросійщених українців переходити на мову, а також показати безкрайні її можливості тим, хто вже говорить українською.

Напевно, цілком логічно, що на шляху до втілення такої нестандартно ідеї ми весь час шукаємо — і треба сказати, успішно знаходимо — нестандартні рішення. На момент, коли я пишу цю статтю, пройшло вже пів року з моменту першого релізу. Із застосунком усе добре, він росте та розвивається, оплати проходять. Хоча було нелегко, але мені вдалося зробити те, що мене не робив ніхто, і я пишаюся цим.

Також мені подобаються широкі можливості, які пропонують як Flutter, так і українська платіжна система.

Запрошую вас переконатися особисто в тому, що всі оплати, зокрема, донати в застосунку працюють. Цим ви також підтримаєте нас на шляху до мети: мінімум 40 мільйонів людей, які із задоволенням говорять українською. Завантажуйте застосунок тут.

👍ПодобаєтьсяСподобалось10
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Краще розкажіть шлях боротьби з техпідтримкою LiqPay по активації послуги, бо інтеграція LiqPay + GooglePay зроблена і протестована ще 3 роки тому, але все заглохло на активації з дивними забаганками LiqPay (ну ще й війна це все відклала)

Не можу сказати, що був якийсь «шлях боротьби», вони просто кидали в мене тех документацією практично на будь-який запит :)

Також знайшов у LiqPay можливість авторизувати певну картку з отриманням спеціального токену для подальших покупок та потім побачив, як дрібненьким шрифтом на сайті LiqPay було написано, що для такого типу інтеграції мені треба мати спеціальні сертифікації, підтвердження і все в такому дусі.

Це так і не так. Бо той сертифікат (PCI DSS) потрібен тільки тоді, коли ви приймаєте дані картки самі, без використання LiqPay (та будь-яких інших платформ). Або якщо хочете тільки зберегти дані картки без проведення оплати. Якщо ж оплата проходить через віджет LiqPay, то в колбеку на сервер ви отримаєте токен, який можете використовувати для прямих списань, вже без участі користувача.

Так от є певний хак, щоб просто «зачепити» картку — робити hold на 1 грн та одразу unhold. Так ви отримаєте токен, з яким потім можете автоматично робити списання.

www.liqpay.ua/...​ion/api/aquiring/hold/doc
www.liqpay.ua/...​ion/api/aquiring/paytoken

Дякую Вам за коментар і рекомендацію! Треба буде добавити таку можливість в API в публічну бібліотеку, яку виставив.

Хех, не перший)
Додаток Зелений Слон 7 вже давно використовує Liqpay + Flutter.
Бо я його робив)

Молодець :) а чому не поділились досвідом з громадою? 🙃

Типова задача для оплати в магазинах. Робили щось схоже, але ссилка на сторінку оплати формується на стороні back end і виводиться на екрані застосунка. +/- для основних платіжних систем(PAYPAL, WAY FOR PAY, LIQPAY, MONOBANK) є приклади в їх документації на різних мовах. Складнощі були лише PRZELEWY24 — це польська система оплати. А чому Ви не зробили формування оплати на стороні back end?

Згідно документації на LiqPay ось тут для інтеграції server-server треба виконати наступні вимоги:

1. Мати сертифікат PCI DSS (Payment Card Industry Data Security Standard) відповідного рівня, в залежності від кількості операцій в рік
2. Подати заявку на підключення

в статті власне писав, чому ми не пішли цим шляхом.

На мою думку нема сенсу формувати url для checkout сторінки на сервері, лишні запити туди-сюди, тому і була написана бібліотека, яка трохи спрощує формування всього необхідного для checkout і відкриття цього у внутрішньому браузері.

Є сенс скачати HttpCanary с 4pda та забрати ващі токени и урли і формувати собі шо заманется. Доречи котрі компаніі вже скористались вашим пакетом не підкажете?))

Щодо компаній не знаю, є лише статистика з pub.dev — 68% popularity, на GitHub пару зірочок та форків. Дехто вже докидав хотфікси і консультувались з приводу побудови своїх бібліотек на інших технологіях.

P.S. Ви завжди можете долучитись та покращити бібліотеку ;) я лише за

У вашому коді опрацювання оплати є прогалина. Параметр user_id у вас приймається через query параметр, а достовірність і валідація даних береться з body, тому для ідентичних data і signature можна оформити підписки для різних користувачів міняючи user_id в query. Щоб цього уникнути вам потрібно передавати user_id в liqpay при формуванні запиту(наприклад в info) і після оплати його отримувати в data. В такому випадку user_id неможливо буде змінити, так як data не пройде валідацію по signature.

Дякую за пильність і рекомендації! Обовʼязково глибше досліджу цей момент, «таску» вже заніс ;)

У нас є мобільний SDK під flutter github.com/...​udipsp/flutter-mobile-sdk

Чудово! Як я розумію це Fondy так? Дякую, що робите і віддали в open source :) В LiqPay не було :)

Автор может где-то и первый но точно не тут

Не хочу розчаровувати автора, але тут він точно не перший, хто зробив таку інтеграцію)) Загалом, хороша лабораторна робота + закритий пунктик якогось performance review.

Ми таки робили інтеграцію через браузер. Результат вертали в додаток через deep link. Прив"язки до flutter мінімум.

З webview мені особисто рішення виглядає менш безпечним з точки зору прокачаного користувача. І от навіть той же facebook зараз не дозволяє робити логін в webview — хоче браузер. Звичайно, можна нашаманити, щоб працювало, але це недовговічно

Дякую за ваш коментар! Було б цікаво дізнатись більше.

Яким чином ви повертали результат покупки назад в застосунок через deep link з checkout екрану LiqPay? Ваше рішення є десь в open source, тому що я не знайшов прикладів ні в офіційній документації LiqPay, ні від будь-кого в публічному доступі?

P.S. Мені вже пізно закривати «пунктики в якомусь performance review» 😅

www.liqpay.ua/...​api/aquiring/checkout/doc

Параметр result_url, який передається при запиті на створення платіжної сторінки.

Публічного коду немає, все для комерційних проектів робили.

Власне як і ми зробили, лише наш result_url веде на бекенд, що безпечніше. Дороблю в мою публічну бібліотеку якийсь цікавий АРІ для роботи без беку за допомогою діп лінки.

Дякую

для бекенда є server-url, по якому методом POST дані ідуть серверу. Клієнт отримує тільки колбек за допомогою result_url, щоб процес оплата закінчився(навіть без інфи про success чи fail) і жодних даних про транзакцію

Дякую за статтю! Нещодавно інтегрували Stripe у React Native app (Expo), щоправда для оформлення підписки, теж пішли шляхом відкриття веб-сторінки всередині додатку. До цього так само інтегрувались з Easypay. Ні в Гугла, ні в епла питань до такого способу не виникло.

В Google Play в принципі питань мало 😅 для прикладу, в нас були екрани для планшетів (які ми підтримуємо, але ще не адаптували інтерфейс), так-от 4 релізи жодних питань до тих скріншотів не було в AppStore. На 5-й реліз прийшли і сказали «Ей! В вас тут смартфон замість планшету на промо для планшетів, бігом приберіть» 🤷🏻‍♂️

Мабуть, ми попали під гарячу руку або хтось має зуб на LiqPay в AppStore, або вам повезло з вашою інтеграцією 💪

Дякую вам за коментар!

Підписатись на коментарі