Stripe 는 매우 편리한 서비스들을 제공하는 PG 기반 핀테크 업체이다.
아직 한국에서는 많이 알려지지 않은 것 같지만,
단순히 PG 를 넘어서, Dashboard 에서 상품등록 까지 할 수 있다.
즉, 누구나 손쉽게 자신만의 e-commerce 를 구축할 수 있도록 도와준다.
회사에서 이번 제품의 결제 기능을 Stripe 를 사용하기로 결정했다.
세계적인 스타트업인 stripe 는 국내 대기업인 카카오보다 훨씬 더
개발자들에게 친화적이다.
API Docs 가 매우 친절하게 작성되어있다.
(카카오는 정말 아직 한~~참 멀었다)
이번 글에서는 @stripe/stripe-react-native 없이, stripe 를 구축하는 법을 소개하겠다.
준비물은 다음과 같다
이 글을 많이 참조했다. 같이 보면 좋다.
하지만 무려 2년전 글이라,
1. Class Component 로 작성함 -> 매우 끔찍
2. stripe API 규칙이 지금과 다름 -> 지금은 저대로하면 작동 안함
저 글 그대로 따라하면 지금은 작동하지 않는다.
따라서,
2022년 현재 상황의 맞게 재구성하였다.
본글에서는 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,
},
})
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
이 생성된다.
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)
})
})
}
위에서 봤겠지만, 결제절차를 진행하는데 있어 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 를 사용해서 구축하는 법을 알아보겠다.