SDK 없이 Stripe 구축하기 | React Native with Stripe

최수민·2022년 11월 9일
0

React Native with Stripe

목록 보기
1/1

Stripe 란?

Stripe 는 매우 편리한 서비스들을 제공하는 PG 기반 핀테크 업체이다.

아직 한국에서는 많이 알려지지 않은 것 같지만,
단순히 PG 를 넘어서, Dashboard 에서 상품등록 까지 할 수 있다.
즉, 누구나 손쉽게 자신만의 e-commerce 를 구축할 수 있도록 도와준다.

회사에서 이번 제품의 결제 기능을 Stripe 를 사용하기로 결정했다.

세계적인 스타트업인 stripe 는 국내 대기업인 카카오보다 훨씬 더
개발자들에게 친화적이다.
API Docs 가 매우 친절하게 작성되어있다.
(카카오는 정말 아직 한~~참 멀었다)


Intro

이번 글에서는 @stripe/stripe-react-native 없이, stripe 를 구축하는 법을 소개하겠다.

준비물은 다음과 같다

  • ReactNative WebView
  • React
  • AWS Amplify
  • Stripe 계정

이 글을 많이 참조했다. 같이 보면 좋다.

하지만 무려 2년전 글이라,
1. Class Component 로 작성함 -> 매우 끔찍
2. stripe API 규칙이 지금과 다름 -> 지금은 저대로하면 작동 안함

저 글 그대로 따라하면 지금은 작동하지 않는다.

따라서,
2022년 현재 상황의 맞게 재구성하였다.

1. React Native

본글에서는 Class Component 로 작성된 것을
모두 Function Component 로 손 봐주었다.

import React, { FC, useState } from "react"
import {
  View,
  Text,
  StyleSheet,
  Dimensions,
  ActivityIndicator,
  TouchableOpacity,
  TextInput,
} from "react-native"
import { WebView } from "react-native-webview"
import { API, graphqlOperation, Analytics } from "aws-amplify"
import * as mutations from "../../src/graphql/mutations" 
import { colors } from "../theme" // 필수코드 아님
// import { withAuthenticator } from "aws-amplify-react-native" // 테스트라 Auth 상관없이 일단 진행
// import config from "./aws-exports" // App.js 에서 처리함
// Amplify.configure(config) // App.js 에서 처리함
// Analytics.disable() // App.js 에서 처리함

export const StripeTestScreen = () => {
  const AMPLIFY_URL = "리액트 배포후 얻는 도메인주소"
  const [amount, setAmount] = useState(0.3) //usd
  const [quantity, setQuantity] = useState("1")
  const [screen, setScreen] = useState("product")
  const [initUrl, setInitUrl] = useState(AMPLIFY_URL)
  const [url, setUrl] = useState(AMPLIFY_URL + "payment-init")
  const [loading, setLoading] = useState(true)

  async function createPaymentSession() {
    // hardcode input values, make these dynamic with the values from the logged in user
    const input = {
      amount: amount * quantity,
      total: 2,
      name: "구매자이름",
      email: "구매자@이메일",
    }

    try {
      const result = await API.graphql(graphqlOperation(mutations.createPayment, { input: input }))
      const sessionID = JSON.parse(result.data.createPayment?.body)

      setUrl(initUrl + "payment?session=" + sessionID.id)
      setLoading(false)
    } catch (error) {
      console.error("에러:", error)
    }
  }

  function handleOrder() {
    setScreen("payment")
  }

  function onNavigationStateChange(webViewState) {
    console.log("STARTED: _onNavigationStateChange")
    console.log("webViewState", webViewState)

    if (webViewState.url === initUrl + "payment-init") {
      createPaymentSession()
    }

    if (webViewState.url === initUrl + "payment-success") {
      setScreen("success")
    }

    if (webViewState.url === initUrl + "payment-failure") {
      setScreen("failure")
    }
  }

  function startPayment() {
    console.log("STARTED: startPayment")
    let _url = url
    if (_url === "") {
      _url = initUrl
    }

    console.log("_url", _url)
    console.log("loading", loading)

    return (
      <View style={{ flex: 1, backgroundColor: colors.palette.purple9456FF, paddingVertical: 40 }}>
        {loading && (
          <View style={[styles.loader, styles.horizontal]}>
            <ActivityIndicator animating={true} size="large" color={colors.palette.purple9456FF} />
          </View>
        )}
        <View
          style={{
            position: "absolute",
            backgroundColor: "#fff",
            height: 70,
            width: Dimensions.get("window").width,
            zIndex: 200,
          }}
        />

        <WebView
          mixedContentMode="never"
          source={{
            uri: _url,
          }}
          onNavigationStateChange={onNavigationStateChange}
        />
      </View>
    )
  }

  function showProduct() {
    return (
      <View style={styles.container}>
        <Text style={styles.product}>Product A</Text>
        <Text style={styles.text}>This is a great product which we sell to you</Text>
        <Text style={styles.text}>The price for today is ${amount}</Text>
        <Text style={styles.quantity}>How many items do you want to buy?</Text>
        <View style={{ flex: 1 }}>
          <TextInput style={styles.textInput} onChangeText={setQuantity} value={quantity} />
          <TouchableOpacity style={styles.button} onPress={() => handleOrder()}>
            <Text>Order now</Text>
          </TouchableOpacity>
        </View>
      </View>
    )
  }

  switch (screen) {
    case "product":
      return showProduct()
    case "payment":
      return startPayment()
    case "success":
      return (
        <View style={styles.container}>
          <Text style={{ fontSize: 25 }}>Payments Succeeded :)</Text>
        </View>
      )
    case "failure":
      return (
        <View style={styles.container}>
          <Text style={{ fontSize: 25 }}>Payments failed :(</Text>
        </View>
      )
    default:
      break
  }
}

// export default withAuthenticator(App)

const styles = StyleSheet.create({
  button: {
    alignItems: "center",
    marginTop: 20,
    backgroundColor: "#DDDDDD",
    padding: 10,
  },
  textInput: {
    width: 200,
    borderColor: "gray",
    borderWidth: 1,
    padding: 15,
  },
  quantity: {
    marginTop: 50,
    fontSize: 17,
    marginBottom: 10,
  },
  text: {
    fontSize: 17,
    marginBottom: 10,
  },
  product: {
    fontSize: 22,
    marginBottom: 10,
  },
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "flex-start",
    justifyContent: "flex-start",
    marginTop: 50,
    margin: 10,
  },
  loader: {
    flex: 1,
    justifyContent: "center",
  },
  horizontal: {
    flexDirection: "row",
    justifyContent: "space-around",
    padding: 10,
  },
})

2-1. Amplify - GraphQL

GraphQL 로 Lambda 를 작동시키자

type Mutation {
  createPayment(input: PaymentInput): PaymentResult
    @function(name: "makePayment-${env}")
}

input PaymentInput {
  amount: Float
  total: Int
  name: String
  email: String
}

type PaymentResult {
  statusCode: Int
  body: String
}

그대로 하면 된다.
하지만 이후!!

amplify codegen

을 실행해줘야

src/graphql/mutations 

이 생성된다.

2-2. Amplify - Lambda

stripe 는 보안상의 이유로 결제처리의 모든 코드를 Front-end 에만 두고있지 않다.
Server Side 에도 존재해야 한다.

이를 위해 우리에겐 든~든한 Lambda 가 있다.

stripe API 가 많이 바뀌어서,
이 부분에 해당하는 코드는 본문과 상당부분 차이가 있다.

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
const SECRET_KEY = "겁나 중요한 Stripe 시크릿키이다. Dashboard 에서 확인 가능함"
const AMPLIFY_URL = "리액트 배포후 얻는 도메인주소"

const stripe = require("stripe")(SECRET_KEY)
exports.handler = async (event, context) => {
  try {
    const amount = event.arguments.input.amount
    const name = event.arguments.input.name
    const email = event.arguments.input.email

    const customer = await createCustomer(name, email)

    const session = await createCheckOutSession(amount, customer.id)

    return session
  } catch (error) {
    console.log("오류가 발생했습니드아아아악", error)
  }
}

async function createCustomer(name, email) {
  return new Promise(function (resolve, reject) {
    stripe.customers
      .create({
        email: email,
        name: name,
      })
      .then((customer) => {
        resolve(customer)
      })
      .catch((err) => {
        // Error response
        reject(err)
      })
  })
}

async function createCheckOutSession(amount, customer) {
  //eslint-disable-line

  return new Promise(function (resolve, reject) {
    stripe.checkout.sessions
      .create({
        payment_method_types: ["card"],
        customer: customer,
        line_items: [
          {
            price_data: {
              currency: "usd",
              unit_amount: amount * 100, //? unit_amount 는 cent 단위이다. 그래서 100 곱했음. 참조: https://stripe.com/docs/api/prices/create?lang=node#create_price-unit_amount
              product_data: {
                name: "TEST PRODUCT BY SOOMIN",
                description: "AWESOME PRODUCT",
                images: ["결제창에서 표시할 상품 이미지 url"],
              },
            },
            quantity: 1,
          },
        ],
        mode: "payment",
        success_url: `${AMPLIFY_URL}payment-success`,
        cancel_url: `${AMPLIFY_URL}payment-failure`,
        locale: "en",
      })
      .then((source) => {
        // Success response
        const response = {
          statusCode: 200,
          body: JSON.stringify(source),
        }

        resolve(response)
      })
      .catch((err) => {
        // Error response
        const response = {
          statusCode: 500,
          body: JSON.stringify(err.message),
        }

        reject(response)
      })
  })
}

3. React

위에서 봤겠지만, 결제절차를 진행하는데 있어 redirect 할 url 이 필요하다.
그냥 간단히 React 로 만들고 AWS Amplify 로 배포하자. 10분도 안 걸린다.

다만, 원글 작성자가 2년전 문법을 쓰기때문에
그대로 따라쓰면 역시 작동안한다.
지금 상황에 맞게 바꾸어주었다.

import React from "react";
import "./App.css";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import queryString from "query-string";
import { loadStripe } from "@stripe/stripe-js";

const STRIPE_PUBLIC_KEY = "stripe dashboard 에서 Publishable Key 값 입력";
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY);

// Showing null, because we will show the result in the app and not on the web
function Success() {
  return null;
}

// Showing null, because we will show the result in the app and not on the web
function Failure() {
  return null;
}

// Showing null, because we will show the result in the app and not on the web
function PaymentInit() {
  return null;
}

function Init() {
  return (
    <div className="App">
      <h1>Payment Site</h1>
    </div>
  );
}

async function initStripe() {
  const parsed = queryString.parse(window.location.search);
  const sessionId = parsed.session;

  const stripe = await stripePromise;
  await stripe.redirectToCheckout({
    sessionId,
  });
}

function Payment() {
  initStripe();
  return null;
}

function App() {
  return (
    <Router>
      <Routes>
        <Route exact path="/" element={<Init />} />
        <Route path="/payment" element={<Payment />} />
        <Route path="/payment-init" element={<PaymentInit />} />
        <Route path="/payment-failure" element={<Failure />} />
        <Route path="/payment-success" element={<Success />} />
      </Routes>
    </Router>
  );
}

export default App;

한계점

React Native for Web 에서는 WebView 를 사용할 수 없다.. 😇
물론, react-native-web-webview 패키지가 존재하지만,
onNavigationStateChange
prop 이 없기 때문에 지금 방식으로는 구현이 불가능한다 🥲

다음글에서는 (아마) 보다 간편해보이는
@stripe/stripe-react-native 를 사용해서 구축하는 법을 알아보겠다.

profile
🇳🇿🇰🇷

0개의 댓글