見出し画像

iOSアプリ内課金をテストし尽くす〜第2回 paymentQueue関数(購入時、更新時、重複購入時)〜[Xcode12/iOS14]

NTTレゾナントテクノロジー アジャイルデザイン部の西添です。最近、iOSアプリのアプリ内課金(In-App Purchase)を実装するために調査と実験をしていました。アプリ内課金の実装方法の解説はインターネット上にたくさんありますが、様々な場面でStoreKitがどのように振る舞うのかを紹介したサイトは見かけません。そこで、iOSアプリ内課金をStoreKit Testing in Xcode(以下、Xcodeテストと呼びます)とSandboxで実験して得られた知見を、連載形式でご紹介したいと思います。本連載の実験対象と実験環境については第1回をご覧ください。

iOSアプリ内課金を実装する上で、SKPaymentTransactionObserverのpaymentQueue(_:updatedTransactions:)関数がどのタイミングで呼ばれ、引数のトランザクションの中身がどうなっているのかを正しく把握しておく必要があります。第2回となる本記事ではこの関数に注目し、初めての購入時、自動更新時、重複購入時の3場面における振る舞いを紹介します。困ったことにXcodeテストとSandboxテストで挙動が微妙に違うので、それも合わせて説明していきます。

連載目次

第1回 導入編
第2回 paymentQueue関数(購入時、更新時、重複購入時) ←本記事
第3回 paymentQueue関数(Interrupted Purchase、Ask to Buy)
第4回 paymentQueue関数(解約後の再契約)
第5回 finishTransaction()を実行しないとどうなるか
↓今後公開予定↓
第6回 購入に失敗するケース
第7回 レシート

場面1. 初めて購入したとき

[Xcodeテスト]

画像1

アプリ内の購入ボタンを押すとAppStoreの課金シートが表示され、同時にpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasingです。このときのコンソールログは次のとおりです。print文の出力方法については第1回のソースコードをご確認ください。

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]

課金シートで承認ボタンを押した後、「You're all set. Your purchase was successful. [Environment: Xcode]」というダイアログが表示されます。端末の言語設定が日本語であっても、Xcodeテストでは英語のダイアログが表示されます。そのダイアログでOKを押すとpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasedです。Transaction IDは0から始まり、購入・自動更新・復元の度に1ずつインクリメントされていきます。

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:13]
 ---------- transaction[0] ----------
 Transaction ID = Optional("3"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:13]

[Sandboxテスト]

画像2

Sandboxテストも基本的にXcodeテストと同様の挙動になりますが、ダイアログは端末の言語設定で設定された言語(日本語)で表示されます。ダイアログの文言は「完了しました。購入手続きが完了しました。[Environment: Sandbox]」でした。また、Transaction IDは16桁の数字でした。

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:04:05]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000781098988"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:04:05]

場面2. サブスクリプションが自動更新されたとき

Xcodeテストで自動更新サブスクリプションを購入する場合、Transaction Managerで解約したりTransactionを削除したりするまでは自動で更新され続けます。一方、Sandboxテストでは5回自動更新された後に自動で解約されます。

更新時のTransactionのIDは毎回変わりますが、Original Transaction IDは常に初めてサブスクリプションを開始したときのTransaction IDとなります。

Sandboxテストでのサブスクリプションは挙動が不安定ですので、注意が必要です。私の経験則ですが、Sandboxでサブスクリプションの自動更新をテストする際は、テストの度に新しいSandbox Apple IDを作成するほうが挙動が安定していそうです。一度でも購入したことがあるSandbox Apple IDでサブスクリプションを開始すると、自動更新が走らず、[設定アプリ]>[App Store]>[Sandboxアカウント]>[管理]で解約済みであるように表示されてしまう事象がありました。不思議なことに、その後数時間経ってからアプリを起動するとpaymentQueue(_:updatedTransactions:)が呼ばれ、引数updatedTransactionsに更新回数分のTransaction (status=.purchased)が入っていたりしました。サブスクリプションの解約直後に再購入すると、この事象が発生しやすいようです。解約から2時間ほど経ってから再購入すると、正常に自動更新されるようになったこともありました。

サブスクリプションが更新された際のpaymentQueue(_:updatedTransactions:)の振る舞いは、XcodeテストとSandboxテストで微妙に違います。以下では、更新を迎えるときのアプリの状態を3パターンに分類して、それぞれの挙動を紹介します。

場面2-1. アプリがフォアグラウンドにある状態で更新を迎える場合

[Xcodeテスト]
サブスクリプションが更新されるタイミングでリアルタイムにpaymentQueue(_:updatedTransactions:)が呼ばれます(transactionState=.purchased)。引数updatedTransactionsの中身は1件のみです。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:13]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:13]

▼1回目の更新

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:39]
 ---------- transaction[0] ----------
 Transaction ID = Optional("2"), State = .purchased
 Original Transaction ID = Optional("1"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:39]

▼2回目の更新

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:10]
 ---------- transaction[0] ----------
 Transaction ID = Optional("3"), State = .purchased
 Original Transaction ID = Optional("1"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:10]

▼3回目の更新

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:41]
 ---------- transaction[0] ----------
 Transaction ID = Optional("4"), State = .purchased
 Original Transaction ID = Optional("1"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:41]

[Sandboxテスト]
Xcodeテストと違い、Sandboxテストの場合はフォアグラウンド状態のままではpaymentQueue(_:updatedTransactions:)は呼ばれません。更新を迎えた後、アプリをバックグラウンドにするためにホーム画面を表示し、それからフォアグラウンドに戻すと呼ばれます。引数updatedTransactionsには更新回数分のtransactionが詰め込まれています。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:32]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000783398989"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:32]

▼フォアグラウンド状態のまま待機。5回目の更新が終わった後にホーム画面を表示し、アプリの履歴からアプリを起動

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:39:02]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000783401812"), State = .purchased
 Original Transaction ID = Optional("1000000783398989"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[1] ----------
 Transaction ID = Optional("1000000783405100"), State = .purchased
 Original Transaction ID = Optional("1000000783398989"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[2] ----------
 Transaction ID = Optional("1000000783407492"), State = .purchased
 Original Transaction ID = Optional("1000000783398989"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[3] ----------
 Transaction ID = Optional("1000000783409267"), State = .purchased
 Original Transaction ID = Optional("1000000783398989"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[4] ----------
 Transaction ID = Optional("1000000783411750"), State = .purchased
 Original Transaction ID = Optional("1000000783398989"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:39:02]

場面2-2. アプリがバックグラウンドにある状態で更新を迎える場合

この場合はXcodeテストとSandboxテストで挙動が異なります。

[Xcodeテスト]
アプリをフォアグラウンドにしたタイミングでpaymentQueue(_:updatedTransactions:)が更新回数分呼ばれます。それぞれの引数updatedTransactionsの中身は1件のみです。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:08]
 ---------- transaction[0] ----------
 Transaction ID = Optional("5"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:08]

▼サブスクリプション開始後、アプリをバックグラウンドにして130秒待ってからフォアグラウンドにした

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:48]
 ---------- transaction[0] ----------
 Transaction ID = Optional("6"), State = .purchased
 Original Transaction ID = Optional("5"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:48]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:48]
 ---------- transaction[0] ----------
 Transaction ID = Optional("7"), State = .purchased
 Original Transaction ID = Optional("5"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:48]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:48]
 ---------- transaction[0] ----------
 Transaction ID = Optional("8"), State = .purchased
 Original Transaction ID = Optional("5"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:48]

[Sandboxテスト]
場面2-1と同様です。アプリをフォアグラウンドにするとpaymentQueue(_:updatedTransactions:)が1回呼ばれます。引数updatedTransactionsには更新回数分のtransactionが詰め込まれており、いずれもtransactionStateは.purchasedです。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:23]
 ---------- transaction[0] ----------
 Transaction ID = Optional(""1000000783506962""), State = .purchased
 Original Transaction ID = Optional(""1000000783433538""), State = Optional("".purchased"")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:23]

▼サブスクリプション開始後すぐにホーム画面を表示してバックグラウンド状態にしておき、15分後にフォアグラウンドにした

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:15:44]
 ---------- transaction[0] ----------
 Transaction ID = Optional(""1000000783508590""), State = .purchased
 Original Transaction ID = Optional(""1000000783433538""), State = Optional("".purchased"")
 transaction.error = nil
 ---------- transaction[1] ----------
 Transaction ID = Optional(""1000000783510182""), State = .purchased
 Original Transaction ID = Optional(""1000000783433538""), State = Optional("".purchased"")
 transaction.error = nil
 ---------- transaction[2] ----------
 Transaction ID = Optional(""1000000783512852""), State = .purchased
 Original Transaction ID = Optional(""1000000783433538""), State = Optional("".purchased"")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:15:44]

場面2-3. アプリが終了されている状態で更新を迎える場合

[Xcodeテスト]
場面2-1、2-2と挙動が異なります。むしろSandboxテストと同じ挙動になります。
アプリを起動するとpaymentQueue(_:updatedTransactions:)が1回呼ばれます。引数updatedTransactionsには更新回数分のtransactionが詰め込まれており、いずれもtransactionStateは.purchasedです。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:21]
 ---------- transaction[0] ----------
 Transaction ID = Optional(""12""), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:21]

▼アプリをタスクキルし、130秒後にXcodeでRunしてアプリを起動

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:52]
 ---------- transaction[0] ----------
 Transaction ID = Optional(""15""), State = .purchased
 Original Transaction ID = Optional(""12""), State = Optional("".purchased"")
 transaction.error = nil
 ---------- transaction[1] ----------
 Transaction ID = Optional(""13""), State = .purchased
 Original Transaction ID = Optional(""12""), State = Optional("".purchased"")
 transaction.error = nil
 ---------- transaction[2] ----------
 Transaction ID = Optional(""14""), State = .purchased
 Original Transaction ID = Optional(""12""), State = Optional("".purchased"")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:52]

[Sandboxテスト]
場面2-1、2-2と同様です。

▼サブスクリプション開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:38]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000783433538"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:38]

▼アプリをタスクキルし、19分後にXcodeでRunしてアプリを起動

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:19:35]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000783435693"), State = .purchased
 Original Transaction ID = Optional("1000000783433538"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[1] ----------
 Transaction ID = Optional("1000000783438080"), State = .purchased
 Original Transaction ID = Optional("1000000783433538"), State = Optional(".purchased")
 transaction.error = nil
 ---------- transaction[2] ----------
 Transaction ID = Optional("1000000783440985"), State = .purchased
 Original Transaction ID = Optional("1000000783433538"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:19:35]

場面3. サブスクリプション期間中に重複購入したとき

この場合はXcodeテストとSandboxテストでpaymentQueue(_:updatedTransactions:)の挙動の差はありません。しかし、重複購入をサブスクリプション開始時と同じ端末でやるか、別の端末でやるかでtransactionStateに差があります。

場面3-1. 同じ端末で重複購入する場合

[Xcodeテスト]
アプリ内の購入ボタンを押すとpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasingです。
同時に、「You're currently subscribed to this. Your 1-month subscription renews on MMM dd, yyyy for zzz. To review subscription settings or cancel this subscription, tap Manage. [Environment: Xcode]」というダイアログが表示されます。
端末の言語設定が日本語であっても、このように英語のダイアログが表示されます。

画像3

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]

ダイアログでOKを押すとpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasedで、Transaction IDはサブスクリプション開始時とは異なります。Original Transaction IDにはサブスクリプション開始時のTransaction IDが入ります。

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:51]
 ---------- transaction[0] ----------
 Transaction ID = Optional("5"), State = .purchased
 Original Transaction ID = Optional("4"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:51]

[Sandboxテスト]
アプリ内の購入ボタンを押すとpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasingです。
数秒待つと「現在サブスクリプションを利用中です。 ●●のサブスクリプションがyyyy年M月d日に¥▲▲で更新されます。サブスクリプションのオプションの確認またはキャンセルを行うには「管理」をタップしてください。 [Environment: Sandbox]」というダイアログが表示されます。

画像4

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]

ダイアログでOKを押すとpaymentQueue(_:updatedTransactions:)が呼ばれます。transactionStateは.purchasedで、Transaction IDはサブスクリプション開始時とは異なります。Original Transaction IDにはサブスクリプション開始時のTransaction IDが入ります。

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:30]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000781101966"), State = .purchased
 Original Transaction ID = Optional("1000000781098988"), State = Optional(".purchased")
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:30]

場面3-2. 別の端末で重複購入する場合

[Xcodeテスト]
Xcodeテストでこの場合を再現することは不可能です。XcodeテストにはAppleアカウントの概念が存在せず、トランザクションが端末ごとに管理されているからです。

[Sandboxテスト]
事前に端末Aと端末Bで同じSandboxアカウントにサインインしておきます。
それから、端末Aでアプリ内の購入ボタンを押し、サブスクリプションを開始します。

▼端末Aでサブスクリプションを開始

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:00]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:00]
 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:00:52]
 ---------- transaction[0] ----------
 Transaction ID = Optional("1000000786271795"), State = .purchased
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:00:52]

次に、端末Bでアプリ内の購入ボタンを押します。すると、「場面3-1. 同じ端末で重複購入する場合」と同様にpaymentQueue(_:updatedTransactions:)が呼ばれ(transactionStateは.purchasing)、「現在サブスクリプションを利用中です。」のダイアログが表示されます。

▼端末Bで購入ボタン押下

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:21]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .purchasing
 Original Transaction ID = nil, State = nil
 transaction.error = nil
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:21]

ダイアログでOKを押した後の挙動は「場面3-1. 同じ端末で重複購入する場合」と異なります。paymentQueue(_:updatedTransactions:)が呼ばれますが、transactionStateは.failedで、error codeは0 (SKError.unknown)です。
NSUnderlyingErrorの中身は「Error Domain=ASDServerErrorDomain Code=3532 "現在サブスクリプションを利用中です。"」です。

▼端末BでダイアログでOKを押下後

 paymentQueue(_:updatedTransactions:) >>>>>>>>>> [0:01:33]
 ---------- transaction[0] ----------
 Transaction ID = nil, State = .failed
 Original Transaction ID = nil, State = nil
 transaction.error = Optional(Error Domain=SKErrorDomain Code=0 "An unknown error occurred" UserInfo={NSLocalizedDescription=An unknown error occurred, NSUnderlyingError=0x2812dc7e0 {Error Domain=ASDServerErrorDomain Code=3532 "現在サブスクリプションを利用中です。" UserInfo={NSLocalizedDescription=現在サブスクリプションを利用中です。}}})
 error.domain = SKErrorDomain, error.code = 0
 error.userInfo = ["NSUnderlyingError": Error Domain=ASDServerErrorDomain Code=3532 "現在サブスクリプションを利用中です。" UserInfo={NSLocalizedDescription=現在サブスクリプションを利用中です。}, "NSLocalizedDescription": An unknown error occurred]
 error.localizedDescription = An unknown error occurred
 error.localizedFailureReason = nil
 error.localizedRecoveryOptions = nil
 error.localizedRecoverySuggestion = nil
 .failed> an unknown or unexpected error occurred.
 paymentQueue(_:updatedTransactions:) <<<<<<<<<< [0:01:33]

宣伝

NTTレゾナントテクノロジーでは一緒に働いてくれるAndroid/iOSアプリエンジニアを募集中です。もし興味がありましたら採用ページをご覧ください。

この記事が気に入ったらサポートをしてみませんか?