Реализация общего компонента для платежного функционала

Реализация общего компонента для платежного функционала

- Оплата Опыт реализации SDK в виде повторно используемого общего компонента React -

1. Предпосылки реализации общего компонента SDK

Бизнес-логика, которую необходимо было обрабатывать непосредственно на экране оплаты, включала гораздо больше, чем простой вызов SDK.

  • Установка ключей клиента для авторизованных и неавторизованных пользователей

  • Отражение изменений суммы оплаты в режиме реального времени

  • Создание и интеграция данных об оплате с бэкендом

  • Одновременная поддержка встроенных и всплывающих способов оплаты

  • Обработка перенаправлений при успешной и неуспешной оплате

  • Обновление статуса на бэкенде при возникновении ошибки оплаты

  • Применение логики для предотвращения дублирующихся запросов на оплату

  • Поддержка среды React Native WebView

Реализация этой логики отдельно на каждом экране не только увеличила бы дублирование кода, но и значительно усложнила бы поддержание единообразия правил обработки платежей. Например, могли возникнуть проблемы из-за фрагментации: один экран сохранял бы ошибки SDK на бэкенде, тогда как другой просто выводил бы их в консоль.

Нам также пришлось учитывать, что каждому сервису требовался свой пользовательский интерфейс оплаты. Поскольку условия оплаты и конфигурации способов оплаты различались, а положение и дизайн кнопки оплаты отличались от сервиса к сервису, включить их в фиксированном виде в общий компонент было практически невозможно.

Поэтому мы разработали структуру, в которой общий компонент отвечал за интеграцию с SDK и управление состоянием оплаты, а каждый экран мог гибко определять момент выполнения фактической оплаты.

2. Ожидаемые преимущества

Цели, которых мы стремились достичь с помощью реализации общего компонента, были следующими.

Во-первых, мы минимизировали связанность между экранами и SDK. Отдельные экраны были спроектированы так, чтобы передавать только необходимые данные, такие как номер заказа, название заказа, сумму и URL перенаправления, без необходимости знать о сложном процессе инициализации SDK.

Во-вторых, мы обеспечили единообразие правил обработки платежей. Обрабатывая весь процесс — от создания данных об оплате до окончательного подтверждения и сохранения статуса ошибки, — в общей области, мы стремились предотвратить ошибки, вызванные различиями в поведении экранов.

В-третьих, мы интегрировали встроенные и всплывающие способы через единый интерфейс. Пользователю нужно лишь выбрать способ оплаты в качестве опции, а во время выполнения он может получить единообразный опыт разработки, вызывая один и тот же метод executePayment().

В-четвёртых, мы повысили эффективность сопровождения. Когда параметры SDK меняются или пересматриваются политики обработки ошибок, мы можем быстро внести изменения, обновив общий компонент в одном месте, вместо модификации каждого экрана по отдельности.

3. Подход к реализации

3.1 Предоставление внешнего интерфейса выполнения оплаты

Формирование интерфейса кнопки оплаты было оставлено на усмотрение экрана каждого сервиса, тогда как общий компонент должен был предоставлять через свой интерфейс только логику выполнения оплаты. Для этого мы использовали `forwardRef` и `useImperativeHandle` из React.

export interface RequestPaymentContainerRef {
  executePayment: () => Promise<void>;
}

useImperativeHandle(ref, () => ({
  executePayment: async () => {
    if (!innerPaymentRef.current) {
      throw new Error('Payment component is not ready.');
    }
    await innerPaymentRef.current.executePayment();
  },
}));

Родительский экран, использующий компонент, может легко инициировать оплату через предоставленный `ref`.

const paymentRef = useRef<RequestPaymentContainerRef>(null)

const handlePayment = async () => {
  await paymentRef.current?.executePayment();
};

В частности, executePayment был предоставлен в виде Promise<void>, чтобы вызывающая сторона могла однозначно определить ошибки выполнения. Если его вызывали, когда компонент оплаты ещё не был готов, он выбрасывал исключение, побуждая родительский экран предоставить соответствующее уведомление.

3.2 Разделение встроенной и всплывающей оплаты

При встроенной оплате способы оплаты и условия отображаются непосредственно на странице, тогда как при всплывающей оплате открывается окно оплаты, а данные создаются при вызове executePayment().

Управление этими различными механизмами в одном компоненте сделало бы его крайне сложным. Поэтому мы разделили роли между `RequestPayment` и `RequestPaymentPopup`, а родительский контейнер настроили на выборочное их использование.

Хотя их внутренние реализации различаются, мы сохранили одинаковый внешне предоставляемый интерфейс, чтобы обеспечить полиморфизм.

3.3 Настройка перенаправлений при успешной и неуспешной оплате

Завершение оплаты в окне оплаты SDK не означает, что вся бизнес-логика выполнена. Для окончательного подтверждения необходимо передать в API подтверждения на бэкенде такую информацию, как `paymentKey`, полученную из URL перенаправления.

Общий процесс выглядит следующим образом.

  1. Зарегистрировать предварительную информацию об оплате на бэкенде и получить уникальный идентификатор платежа.

  2. Указать выданный идентификатор в качестве `orderId` и запросить оплату через SDK.

  3. После обработки платежа платёжным шлюзом в окне оплаты перейти на назначенный URL успешного или неуспешного завершения.

  4. При успешной оплате вызвать API окончательного подтверждения на бэкенде.

  5. При неуспешной оплате обновить статус платежа на бэкенде, установив значение «ошибка».

  6. После завершения всей последующей обработки перейти на конечный экран назначения.

Для этого мы разработали промежуточные маршруты-мосты `success-redirect` и `fail-redirect`. Завершая бизнес-логику на этом этапе, мы чётко разграничили «успешную оплату через платёжный шлюз» и «завершение внутренней обработки данных», повысив согласованность данных.

4. Аспекты, учитывавшиеся при реализации

4.1 Инициализация и доступность оплаты

Сам факт создания экземпляра SDK не означает, что оплата становится доступной немедленно. При использовании встроенного способа необходимо полностью завершить отображение интерфейса способов оплаты и условий.

Поэтому мы отдельно и строго управляли объектом SDK, виджетами и состоянием завершения фактического отображения — ready. Чтобы предотвратить инциденты, связанные с оплатой, мы применяли защитные меры, например отключали кнопку до завершения отображения.

Кроме того, поскольку при изменении суммы, переданной через props, необходимо синхронизировать и внутреннее состояние SDK, мы добавили логику для соответствующего повторного вызова widgets.setAmount().

4.2 Предотвращение дублирующихся запросов на подтверждение

API подтверждения вызывается при монтировании экрана перенаправления после успешной оплаты, однако один и тот же запрос может быть выполнен несколько раз из-за таких действий, как обновление страницы или возврат назад.

Полностью заблокировать это только с помощью управления состоянием на фронтенде сложно. Поэтому крайне важно спроектировать систему так, чтобы гарантировать идемпотентность на уровнях бэкенда и API платёжного шлюза с учётом повторных сетевых запросов и среды с несколькими вкладками.

4.3 Среды URL и маршрутизации

Поскольку общие компоненты используются в различных средах маршрутизации, важно правильно нормализовать `basePath`. При добавлении параметров запроса бесперебойность платёжного процесса можно обеспечить только при тщательной обработке, например при выборе правильного разделителя (`?` или `&`) и кодировании сообщений об ошибках.

Хотя на первый взгляд это может показаться простой задачей, во встроенных средах или при сложной структуре путей эти базовые операции становятся критически важными факторами, определяющими успешность платежа.

4.4 Среды Popup и WebView

Поскольку метод popup регистрирует обработчики событий во время выполнения, необходимо предотвращать повторную регистрацию обработчиков, вызванную многократными нажатиями кнопки. Мы тщательно реализовали управление состоянием во время выполнения и логику очистки при размонтировании компонента.

В частности, мы добавили поддержку внедрения `appScheme` для сред React Native WebView. Отказавшись от жёсткого кодирования и обеспечив динамическую настройку среды через параметры приложения, мы учли возможность дальнейшего расширения.

5. Заключение

Реализация простого платёжного окна с помощью платёжного SDK не представляла сложности. Однако настоящим вызовом стало последовательное применение сложного рабочего процесса — от инициализации до управления состоянием на стороне бэкенда — во всей организации.

В рамках этого проекта мы максимально повысили удобство использования, разделив платёжную функциональность по ролям и предоставив инкапсулированный интерфейс через `forwardRef`.

После внедрения общих компонентов заметно снизились затраты и количество человеческих ошибок при добавлении платёжной функциональности в новые сервисы, а сопровождаемость системы в целом также улучшилась.

Разумеется, техническая полнота не ограничивается фронтендом. Я считаю, что по-настоящему надёжная платёжная система достигается только тогда, когда обработка идемпотентности на бэкенде и логика согласования состояний работают совместно.

Это был ценный опыт, подтвердивший, что повторно используемые компоненты не просто устраняют дублирование кода: они изолируют сложность внешних библиотек и обеспечивают единообразную бизнес-ценность во всём сервисе.

chnsik

Site footer