Hello Tech

AutoReserve 等を開発する株式会社ハローのテックブログです。スタートアップの最前線から本質的な価値を届けるための技術を紹介します。

グローバルアプリケーションにおけるマルチStripeアカウントの実践

AIによって世界中のレストラン予約を行うサービス、AutoReserveのフロントエンドを担当している星野です。

AutoReserveの新規ユーザーは日々増え、約4割を海外の方が占めています。私たちはより多くのユーザーの幅広いニーズにお応えし、サービスを快適に利用いただけるよう、機能改善と新たな仕組みの導入に取り組んでいます。私はユーザーエンゲージメント向上を目的に、CVRやリピート率などのKPIを追っています。当社ではエンジニアもOKRの責任者となり、ビジネス数値を日常的に扱います。

本記事ではここ最近行った施策の一つを取り上げます。

多様な決済手段に対応する

海外ユーザーの多くは、主に訪日の旅行でレストランを予約します。中華圏のユーザーは特に食への関心が高く、有名店を多くの人数で訪れる傾向にあります。

与信確保やコース予約に伴う前払いのため、レストラン予約時に決済方法の登録をお願いしています。有効な決済手段がないと離脱を生み出してしまうため、できるだけ多くの決済手段を提供する必要があります。

中華圏においてはUnionPay(銀聯)が圧倒的な決済シェアを誇っています。この巨大なマーケットにアプローチするためには、UnionPayへの対応は避けて通れません。

UnionPayは中華圏、特に中国本土において、VisaやMastercardを凌駕する圧倒的なシェアを持つ決済ネットワークです。香港、マカオ、台湾でも広く受け入れられており、中華圏全体で非常に便利な決済手段と言えます。 by Gemini 2.5 Pro

AutoReserveでは決済プラットフォームとしてStripeを採用しています。しかし現状使用しているStripeアカウントの国は日本であり、UnionPayは日本のアカウントでは利用できませんでした。

support.stripe.com

アカウントの国によって利用できる決済手段が異なることはしばしばあるため、注意が必要です。国によって主要な決済手段が異なるため、ローカライズを強化していく過程で継続的な調査が必要です。

カードブランド Stripe アカウントの国 購入者の国 3D セキュア認証 ウォレット
銀聯 オーストラリア、カナダ、香港、マレーシア、ニュージーランド、シンガポール、イギリス、アメリカ、欧州経済領域 (EEA) グローバル 対応 未対応

日本のStripeアカウントで非対応の決済手段を追加するため、Stripeの新たなアカウントとしてイギリス(uk)を追加することにしました。会社としてロンドンに拠点を設ける計画が進行中で、イギリスアカウントの開設が容易だったためです。今回は、ユーザがUnionPayを登録した場合はStripe UKアカウントに紐付け、決済時にも同アカウントを経由するよう実装します。

Stripe複数アカウント対応のフロントエンド実装

実装にあたって、Stripe SDK内でアプリケーション内でアカウントを切り替える機能が用意されていないという問題に直面しました。

docs.stripe.com

そのためシンプルではありますが、React Context API経由で使うべきStripeアカウントのregionを渡し、StripeElementsのkeyにregionを渡し再マウントすることでアカウント切り替えを行うというアプローチをとりました。これにより、ユーザーが決済方法を適切に登録できるようになりました。

フロントでの要件を簡潔にまとめると以下になります。

  • JPアカウント用・UKアカウント用のフォーム、2種類表示し、切り替えられるようにする
  • 利用ユーザーの国が中国圏の場合、デフォルトをUnionPay用(= UKアカウント)フォームを選択状態にする
  • アジア圏以外のユーザーの場合、UnionPay用のフォームは表示しない
  • 登録したい決済手段が含まれているフォームをユーザーに選択してもらう

クレジットカード登録画面

上記要件を満たすような実装を一部抜粋します。

StripeRegionProvider

  • 指定中のStripeアカウントを注入するContext
  • ユーザーの国情報 or ユーザーの選択で決定する
export const STRIPE_REGION = {
  jp: 'jp',
  uk: 'uk',
} as const

type StripeState = {
  stripeRegion: keyof typeof STRIPE_REGION
  setStripeRegion: (account: keyof typeof STRIPE_REGION) => void
}

const StripeRegionContext = React.createContext<StripeState | undefined>(
  undefined
)

export function StripeRegionProvider({
  children,
  defaultRegion,
}: {
  children: React.ReactNode
  defaultRegion?: keyof typeof STRIPE_REGION
}) {
  const [stripeRegion, setStripeRegion] = React.useState<
    StripeState['stripeRegion']
  >(defaultRegion ?? 'jp')
  return (
    <StripeRegionContext.Provider value={{ stripeRegion, setStripeRegion }}>
      {children}
    </StripeRegionContext.Provider>
  )
}

export function useStripeRegion() {
  const context = React.useContext(StripeRegionContext)
  if (!context) {
    throw new Error(
      'useStripeRegion must be used within a StripeRegionProvider'
    )
  }
  return context
}

RegionalElements

  • stripeRegionをStripeRegionContext経由で受け取る
  • 値に応じてstripePromiseを初期化
  • stripeRegionをkeyとし再マウントのトリガーとする
  • 決済情報フォームをchildrenとして受け取る
import {
  STRIPE_REGION,
  StripeRegionProvider,
  useStripeRegion,
} from '../StripeRegionProvider'

function RegionalElements({ children }: { children: React.ReactNode }) {
  const { stripeRegion } = useStripeRegion()
  const stripePromise = useMemo(
    () =>
      loadStripe(
        stripeRegion === 'uk'
          ? Config.stripeUKPublishableKey
          : Config.stripePublishableKey
      ).catch((err) => {
        console.warn(err)
        Sentry.captureException(err)
        return null
      }),
    [stripeRegion]
  )

  return (
    <Elements
      stripe={stripePromise}
      key={stripeRegion}
      options={{
        locale: getLocale() as StripeElementLocale,
      }}
    >
      {children}
    </Elements>
  )
}

function RegionalElementsContainer({
  children,
  defaultRegion = 'jp',
}: {
  children: React.ReactNode
  defaultRegion?: keyof typeof STRIPE_REGION
}) {
  return (
    <StripeRegionProvider defaultRegion={defaultRegion}>
      <RegionalElements>{children}</RegionalElements>
    </StripeRegionProvider>
  )
}

export { RegionalElementsContainer as RegionalElements }

CreditCardRegistrationContent

  • 上記の利用
export function CreditCardRegistrationContent({
  onSubmit,
}: {
  onSubmit?: () => Promise<void>
}) {
  const defaultRegion =
    currentUser != null
      ? userCountryCode === 'cn'
        ? STRIPE_REGION.uk
        : STRIPE_REGION.jp
      : STRIPE_REGION.jp

  return (
    <RegionalStripeProvider defaultRegion={defaultRegion}>
      {/* 登録フォーム */}
      <CreditCardRegistrationForm {...props} />
    </RegionalStripeProvider>
  )
}

仕様から分かるように、一点妥協したUXがあります。「決済手段をユーザーに選択してもらう必要がある」という点です。Promise<Stripe>の取得を行うloadStripeにStripeアカウント情報を渡す必要があり、「ユーザーが入力した内容に応じてStripeアカウントを使い分ける」ということが難しい状況でした。ユーザーに「選択してもらう」代わり、デフォルトの選択状態や表示制御をユーザーの国によって制御することで、入力時の混乱を回避するようにしました。フォームの切り替えによってフォームが再マウントされるので、入力状態が破棄されてしまいますが、その状況になるのは稀で、離脱の可能性も低いと判断しこの実装に至りました。

バックエンドとの連携

ユーザーのカード情報はStripeの各アカウントに登録されるため、AutoReserveのDBにもどちらのStripeアカウントを使ったかを保存しておくようにしました。createTokenの返り値token.card.brand(e.g. MasterCard, UnionPay)がUnionPayならUKアカウント、という具合です。決済・カード情報削除等はこのbrandを見てStripeアカウントを制御します。

const handleSubmit = async () => {
    const cardNumberElement = elements.getElement(CardNumberElement)
    
    // Stripe SDKを通じてstripe tokenを発行
    const { token: stripeToken } =
      await stripe.createToken(cardNumberElement)

  // Stripe TokenをDBに保存
  await addUserCreditCard({
    stripe_token: stripeToken.id,
    brand: stripeToken.card?.brand as Parameters<
      typeof addUserCreditCard
    >[number]['brand'],
  })
    
}

まとめ

  • Stripeにおける決済手段はStripeアカウントが属する国によって決まる
  • 日本アカウントでは、中華圏で多くのシェアを誇るUnionPayの利用は現状できない
  • AutoReserveはUKアカウントを追加し、UnionPay決済に対応した
  • Stripe SDKは複数Stripeアカウントの切り替えには対応しておらず、独自で実装する必要があるが不可能ではない

UnionPay対応後、訪日中華圏ユーザーのエンゲージメントが向上しました。カード登録フォームはやや複雑になりましたが、離脱などの副作用はほとんど見られませんでした。

今回はUnionPayの導入にあたりStripeアカウントを使い分ける実装を行いましたが、「複数国に法人を設立し、それぞれの売上をそれぞれのアカウントで管理したい」というケースの方がメジャーなのかなと想定しています。よければ参考にしてみてください。

AutoReserveは今後も世界中のユーザーが快適に利用できるようサービスを拡張していきます。予約対応エリアも順次拡大中ですので、海外旅行の際はぜひご活用ください。

prtimes.jp

株式会社ハローでは、グローバルへのチャレンジ・ビジネスの数値に執着するエンジニアを募集しています。

www.hello.ai