인앱 결제 실패 응답값 처리 정리

Adam·2024년 7월 15일
0

개발일지

목록 보기
2/15

인앱 결제를 진행하면서 결제를 실패했을 때 해당하는 값들을 처리하는 로직을 만들어야 했다.
이를 찾아보니 애플/구글 서버에서 별도의 콜백을 주는 것이 아니고 클라이언트에서 구매를 요청 했을 때 이에 해당하는 응답을 클라이언트에 직접 전달하는 것으로 보인다.
그렇기에 스위프트 같이 네이티브로 개발을 할때와 플러터 같은 크로스플랫폼으로 개발을 할때 방식이 조금 다른데 현 회사에서는 클라이언트로 플러터를 사용하기 때문에 플러터 구현 방식으로 정리를 했다.

Flow

  1. 클라이언트에서 결제 신청
  2. 애플/구글에서 실패응답
  3. in_app_purchase 플러그인에서 해당값 처리
  4. 클라이언트에서 시나리오에 맞는 JSON 생성 후 서버에 전달

Apple

Callback으로 서버쪽에 실패에 해당하는 값을 전달하거나 JSON 응답을 주지 않는다

대신 flutter 클라이언트의 in_app_purchase 패키지에서 처리

클라이언트에서 이를 처리 후 서버쪽에 전달하는 방식으로 DB에 저장해야 된다

Google

마찬가지로 flutter을 사용한다면 in_app_purchase에서 처리 후 서버로 전달하는 방식으로 DB에 저장

예시 코드

import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

class PurchaseScreen extends StatefulWidget {
  
  _PurchaseScreenState createState() => _PurchaseScreenState();
}

class _PurchaseScreenState extends State<PurchaseScreen> {
  final InAppPurchase _iap = InAppPurchase.instance;
  bool _available = true;
  late StreamSubscription<List<PurchaseDetails>> _subscription;

  
  void initState() {
    super.initState();
    _initialize();
    final purchaseUpdated = _iap.purchaseStream;
    _subscription = purchaseUpdated.listen(_listenToPurchaseUpdated, onDone: () {
      _subscription.cancel();
    }, onError: (error) {
      // handle error here
    });
  }

  void _initialize() async {
    final bool available = await _iap.isAvailable();
    setState(() {
      _available = available;
    });
  }

  void _buyProduct(ProductDetails productDetails) async {
    final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails);
    await _iap.buyConsumable(purchaseParam: purchaseParam);
  }

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
    purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
      if (purchaseDetails.status == PurchaseStatus.pending) {
        _showPendingUI();
      } else {
        if (purchaseDetails.status == PurchaseStatus.error) {
          _handleError(purchaseDetails.error!);
        } else if (purchaseDetails.status == PurchaseStatus.purchased ||
            purchaseDetails.status == PurchaseStatus.restored) {
          bool valid = await _verifyPurchase(purchaseDetails);
          if (valid) {
            _deliverProduct(purchaseDetails);
          } else {
            _handleInvalidPurchase(purchaseDetails);
          }
        }
        if (purchaseDetails.pendingCompletePurchase) {
          await InAppPurchase.instance.completePurchase(purchaseDetails);
        }
      }
    });
  }

  void _handleError(IAPError error) {
    String errorJson = jsonEncode({
      "result": "failed",
      "error": {
        "code": error.code,
        "message": error.message,
        "source": error.source,
        "details": error.details,
      },
      "transaction": null
    });
    print(errorJson);
  }

  void _showPendingUI() {
    // Show a loading indicator or some UI indicating the transaction is pending
  }

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    // Implement your purchase verification logic here
    return true;
  }

  void _deliverProduct(PurchaseDetails purchaseDetails) {
    // Deliver the purchased product to the user
  }

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // Handle the case when the purchase is invalid
  }

  
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Purchase Screen"),
      ),
      body: Center(
        child: _available
            ? Text("Store is available")
            : Text("Store is unavailable"),
      ),
    );
  }
}

in_app_purchase 패키지은 PurchaseStatus가 error인지로 확인하고 PurcahseDetails.error을 통해서 에러에 대한 상세 정보를 확인할 수 있다.

in_app_purchase의 PurchaseDetails

profile
Keep going하는 개발자

0개의 댓글