Stripe+Firebase+Flutter の紆余曲折実装物語
昨日からstripeを触っていて、訳のわからないことになって独りでキレているので、記録を作っておこうと思う。
まえがき
以前、Stripe+Firebase+Swiftで請求書払いをやったことがあって、それはアプリからstripeの支払い画面に飛んで支払いをしてもらうという簡単なものだったので特に苦労しなかった。それでも丸1日はかかった気がするが。
今回はユーザーが出品したものをユーザーが購入する、minneとかuberみたいなアプリを作っているので、カード情報の登録・変更・消去→アプリ内で支払い の実装が必要になる。
いろいろなサイトを見たが、数年前に廃止されたAPIの情報だったり、実装方法を全く教えてくれない書き方だったりして、解決できない。公式ドキュメントを見ても「結局どこから始めればいいんだ!!」とキーボードを殴り散らかしたくなる始末である(ちゃんと全部読めばわかるのだろうけど)。
軌跡
① Stripeに登録、FirebaseのExtension設定
② Payment Sheetを表示して支払い
これは簡単だけど、前回の入力内容は覚えてくれてなくて毎回カード情報を入力することになってしまう。そこで③
③ Customer(stripe上の顧客)としてユーザーを登録
Stripe API:https://stripe.com/docs/api/customers/create
package:flutter_stripe/flutter_stripe.dartを使う方法もあるんだろうけど、調べるのがもう面倒になったので直接APIを叩いた。(SignIn.uid()とかはFirebase Authentificationのやつ。Descriptionは適当に日付にしといた)。
そして、作成したCustomerのIDをFirebaseのUserデータに保存する。
import 'package:http/http.dart' as http;
Future<String> createCustomer() async {
var response = await http.post(
Uri.parse('https://api.stripe.com/v1/customers'),
headers: PaymentService.headers,
body: {
'name': SignIn.uid(),
'email': SignIn.email(),
'description': '${DateTime.now().toStr()}',
},
);
var data = json.decode(response.body);
var user = await Ref.getMe();
if (user == null) return "";
user.stripeCustomerId = data["id"];
Ref.registerUser(user, SignIn.uid()!);
print('createCustomer $data');
return data["id"];
}
④ 作成したCustomerに紐ずくカードを作る
ここで混乱した。以下を見てほしい。新しいカード情報をstripeに追加したいのに、bodyにカード情報が必要ない。どうやって追加してるんだよ。
import 'package:http/http.dart' as http;
var response = await http.post(
Uri.parse('https://api.stripe.com/v1/customers/$customerId/sources'),
headers: PaymentService.headers,
body: {
'source': 'tok_mastercard',
},
);
var data = json.decode(response.body);
print('registerCard $data');
上記のコードを走らせて返ってきたレスポンスは以下。なんか登録されてるぞ。
{id: card_1KYM76EsaMMX9cMexO7PGXGr, object: card, address_city: null, address_country: null, address_line1: null, address_line1_check: null, address_line2: null, address_state: null, address_zip: null, address_zip_check: null, brand: MasterCard, country: US, customer: cus_LEnu6o8naYZsIC, cvc_check: null, dynamic_last4: null, exp_month: 3, exp_year: 2023, fingerprint: 13wyWWsWlQM9v5BX, funding: credit, last4: 4444, metadata: {}, name: null, tokenization_method: null}
「package:flutter_stripe/flutter_stripe.dartのCardFieldを使って、カード情報をstripeに送ればいいんでしょ」と思っていたが違うのか。。?というか'tok_mastercard'て何?
'source'について見てみると、以下のように書いてあった。
A token, like the ones returned by Stripe.js. Stripe will automatically validate the card.
'Stripe.js'の文字をタップできるようになっていたのでタップすると、「stripe.jsの全リファレンスを見ろ!」というかんじのページに飛ばされたので撤収。
tokenについて調べてみた。
なんと!ここでカード情報をstripeに送信して、tokenをゲットするのね?
CardFieldで取得した情報でtokenを取得しようと試みた
Future<void> createCard(
String customerId, CardFieldInputDetails card) async {
print('\ncard $card');
var response = await http.post(
Uri.parse('https://api.stripe.com/v1/tokens'),
headers: PaymentService.headers,
body: {
'card[number]': card.number,
'card[exp_month]': card.expiryMonth.toString(),
"card[exp_year]": card.expiryMonth.toString(),
"card[cvc]": card.cvc.toString(),
},
);
var data = json.decode(response.body);
print('createdCardToken $data');
registerCard(customerId, response.body);
}
すると、なんと、「card.numberがnullだよ」というエラーが出現。調べてみたけど何も原因を見つけられなかったので別の方法を模索。下記のページを参考にしながらカード入力欄を作って代用した。
すると以下のエラーが
{error: {message: Invalid string: {
"id": ...: false
}
調べると、You have to create a token with Stripe Checkout or Stripe.js first.と言っているのを見つけた。Stripe Checkoutだと、アプリからstripeの支払いフォームに移動する形になるので、Stripe.jsを使う必要がある。どうやら逃げ場はないようだ。
flutter_stripeにcreateTokenというFunctionがあった!これか〜。
void createToken(
String customerId,
String name,
Address address,
ValueChanged<String> onError) {
print('createToken');
Stripe.instance
.createToken(CreateTokenParams.card(
params: CardTokenParams(
type: TokenType.Card,
name: name,
address: address)))
.then((value) => //print('result.token $value')
registerCard(customerId, value.id)
)
.onError((error, stackTrace) => onError('$error'));
}
またカード情報を送信する場所がない。CardFieldを使うのか。すると。。
{error: {code: resource_missing, doc_url: https://stripe.com/docs/error-codes/resource-missing, message: No such token: '{\"id\":\"tok_1KYOl1EsaMMX9cMeh4Lzsj0f\",\"created\":\"1646114423000\",\"type\":\"Card\",\"livemode\":false,\"bankAccount\":null,\"card\":{\"brand\":\"Visa\",\"country\":\"US\",\"currency\":null,\"expYear\":2023,\"expMonth\":4,\"name\":\"my name\",\"funding\":\"Credit\",\"last4\":\"4242\",\"address\":{\"city\":\"Tokyo\",\"country\":\"Japan\",\"line1\":\"\",\"line2\":\"\",\"postalCode\":\"111111\",\"state\":\"\"}}}', param: source, type: invalid_request_error}}
No such token...? 以下に変更したらできた!(それに応じてregisterCardも変更する)
before: registerCard(customerId, value)
after: registerCard(customerId, value.id)
できた〜〜!次!
⑤ 登録済みのカード一覧を取得。最大10件まで取得できるそうだ。
Future<Map<String, dynamic>> getCustomerCards(String customerId) async {
var response = await http.get(
Uri.parse(
'https://api.stripe.com/v1/customers/$customerId/sources?object=card'),
headers: PaymentService.headers,
);
var data = json.decode(response.body);
print('getCustomerCards $data');
return data as Map<String, dynamic>;
}
④で登録されたカードが取得できた!
⑥ カードの消去も実装。
Future<void> deleteCard(String customerId, String cardId) async {
var response = await http.delete(
Uri.parse(
'https://api.stripe.com/v1/customers/$customerId/sources/$cardId'),
headers: PaymentService.headers,
);
var data = json.decode(response.body);
print('deleteCard $customerId $cardId $data');
}
⑦ ついに支払いを実装!
Future<void> pay(int amount, BuildContext context) async {
var response = await http.post(
Uri.parse('https://api.stripe.com/v1/payment_intents'),
headers: PaymentService.headers,
body: {
'amount': '${amount}',
'currency': 'jpy',
'payment_method_types[]': 'card',
},
);
print('response.body ${response.body}');
var clientSecret = json.decode(response.body);
final billingDetails = BillingDetails(
email: SignIn.email(),
);
await Stripe.instance
.confirmPayment(
clientSecret['client_secret'],
PaymentMethodParams.card(
billingDetails: billingDetails,
setupFutureUsage: PaymentIntentsFutureUsage.OffSession,
),
)
.then((response) => {print("Successfully sent message: $response")})
.onError(
(error, stackTrace) => {print("onError sent message: $error")});
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('お支払いが完了しました')));
}
できたーーーー!!!!!!!!!わーーーーい💰💰💰💰
CardFieldとStripe.instanceの関係が最後までよくわからなかった。裏で繋がってるのか。。とりあえずできたっていうことで、一件落着。
ここまで丸2日。
次はApple Payをやっていこうと思う。