💰

FlutterとNode.jsを連携したPayPay決済の実装方法

に公開

FlutterからNode.jsを使ってPayPay決済を実装する方法

はじめに

こんにちは!個人アプリ開発者の のす です。

私は趣味でFlutterを使ったモバイルアプリ開発を行っており、PayPay決済を導入しようとした際に、Flutter向けの公式SDKが存在しないという課題に直面しました。そこで、Node.jsバックエンドとの連携による実装方法を紹介します。

なぜNode.jsとの連携が必要なのか?

PayPayの決済処理をFlutterアプリに実装する際の最大の課題は、Flutter向けの公式PayPayライブラリが存在しない点です。これにより、バックエンドサーバー(Node.js)を介した連携が必要になります:

  • APIキーのセキュリティ保護: APIキーをクライアント側に保存すると漏洩リスクがある
  • サーバーサイドでの決済処理: 認証と決済処理をサーバー側で行うことでセキュリティを確保
  • PayPayの公式Node.js SDKの活用: 公式のSDKを使うことで実装が容易になる

PayPay決済の実装フロー

PayPay決済を実装するための全体的なフローは以下の通りです:

1. 認証フェーズ

  1. Flutterアプリで「PayPayユーザー認証」ボタンをタップ
  2. Node.jsバックエンドへリクエストを送信
  3. バックエンドがPayPay APIから認証用QRコードURLを取得
  4. Flutterアプリ内のWebViewでQRコードを表示
  5. ユーザーがPayPayアプリで認証
  6. 認証完了後、コールバックでuserAuthorizationIdを取得

2. 決済フェーズ

  1. 取得したuserAuthorizationIdを使って決済画面に遷移
  2. 決済ボタンをタップするとNode.jsバックエンドに決済リクエスト送信
  3. バックエンドがPayPay APIで決済処理を実行
  4. 結果をFlutterアプリに返し、ユーザーに成功/失敗を通知

Flutter側の実装

1. PayPay認証画面の実装

// WebViewでPayPay認証ページを表示
Widget _buildInAppWebView(String url) {
  return InAppWebView(
    initialUrlRequest: URLRequest(url: Uri.parse(url)),
    initialOptions: InAppWebViewGroupOptions(
      crossPlatform: InAppWebViewOptions(
        javaScriptEnabled: true,
        useShouldOverrideUrlLoading: true,
      ),
    ),
    onLoadStop: (controller, url) async {
      if (url != null && url.queryParameters['userAuthorizationId'] != null) {
        userAuthorizationId = url.queryParameters['userAuthorizationId'];
        // 認証が完了したら、次のページに自動的に遷移する
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(
            builder: (context) => CreatePaymentPage(userAuthorizationId),
          ),
        );
      }
    },
  );
}

// PayPay認証処理を開始
Future<void> getAuthorization() async {
  final url = 'あなたのバックエンドURL/auth';
  
  try {
    // Node.jsバックエンドにリクエスト
    final response = await http.post(
      Uri.parse(url),
      headers: {
        'Content-Type': 'application/json',
      },
    );

    // レスポンス処理
    if (response.statusCode == 200) {
      final responseBody = json.decode(response.body);
      final jsonData = responseBody['BODY'];
      final resultInfo = jsonData['resultInfo'];
      final data = jsonData['data'];
      
      if (resultInfo['code'] == "SUCCESS") {
        String linkQRCodeURL = data['linkQRCodeURL'];
        // WebViewでPayPay認証画面を表示
        _launchURL(linkQRCodeURL);
      }
    }
  } catch (e) {
    print('Error occurred: $e');
  }
}

2. 決済実行の実装

// 決済処理の実行
Future<Map<String, dynamic>?> createPayment(
    BuildContext context, int amount) async {
  
  final paymentUrl = 'あなたのバックエンドURL/createpayment?userAuthorizationId=${widget.authorizationId}';

  try {
    final response = await http.post(
      Uri.parse(paymentUrl),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'amount': amount
      }),
    );

    if (response.statusCode == 200) {
      final responseBody = json.decode(response.body);
      
      // 決済成功時の処理
      if (responseBody['status'] == 200 &&
          responseBody['res_body']['resultInfo']['code'] == "SUCCESS") {
          
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('決済成功'),
              content: Text($amountの決済が正常に完了しました。'),
              actions: [
                TextButton(
                  child: Text('閉じる'),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                ),
              ],
            );
          },
        );
      }
      
      return responseBody;
    }
    return null;
  } catch (e) {
    // エラー処理
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('決済失敗'),
          content: Text('決済が完了できませんでした。もう一度お試しください。'),
          actions: [
            TextButton(
              child: Text('閉じる'),
              onPressed: () => Navigator.of(context).pop(),
            ),
          ],
        );
      },
    );
    return null;
  }
}

3. ユーザーインターフェース実装例

class PayPayPaymentPage extends StatefulWidget {
  
  _PaymentScreenState createState() => _PaymentScreenState();
}

class _PaymentScreenState extends State<PayPayPaymentPage> {
  String? userAuthorizationId;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PayPay決済'),
        backgroundColor: Colors.black,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('PayPay決済を始める', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            SizedBox(height: 30),
            ElevatedButton.icon(
              onPressed: getAuthorization,
              icon: Icon(Icons.payment, size: 24),
              label: Text('PayPayユーザー認証'),
              style: ElevatedButton.styleFrom(
                primary: Color.fromARGB(255, 100, 154, 248),
                padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
                textStyle: TextStyle(fontSize: 20),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

バックエンドとの連携

バックエンドの詳細な実装はここでは省略しますが、Flutter側からは以下の2つの主要なエンドポイントを呼び出します:

  1. 認証エンドポイント (/auth):

    • PayPay SDK の AccountLinkQRCodeCreate メソッドを使用
    • QRコードURLを生成して返す
  2. 決済エンドポイント (/createpayment):

    • userAuthorizationIdと金額を受け取る
    • PayPay SDK の CreatePayment メソッドで決済処理
    • 決済結果をFlutterに返す

サーバーレス実装のヒント

個人開発では、Firebase Cloud FunctionsやVercel Serverless Functionsなどのサーバーレスアーキテクチャを活用するとインフラ管理の負担が軽減されます。基本的な実装フローは変わりませんが、デプロイやAPIキー管理が容易になります。

まとめ

Flutter向けの公式PayPay SDKがない現状では、Node.jsバックエンドを介した連携が最も堅実な実装方法です。このアプローチにより、セキュリティを確保しながらPayPay決済機能をFlutterアプリに組み込むことができます。

重要なポイント:

  • ユーザー認証と決済処理の2つのフェーズに分けて実装
  • センシティブな情報や処理はすべてサーバーサイドで行う
  • WebViewを使用してPayPay認証画面を表示
  • userAuthorizationIdを使って決済処理を行う

この記事が、PayPay決済を導入したいFlutterアプリ開発者の参考になれば幸いです。

Discussion