민프

[Flutter] Flutter에서 인앱결제를 해보자 (IOS) 본문

[Flutter]

[Flutter] Flutter에서 인앱결제를 해보자 (IOS)

민프야 2024. 3. 21. 18:34

이번 포스팅에서는 Flutter에서 IOS 인앱결제 하는 부분을 다뤄보려고 합니다.

IOS 인앱결제에 궁금하신 분들은 아래 포스팅을 참고하시길 바랍니다.

https://minf.tistory.com/entry/IOS-AppleIOS%EC%9D%98-%EC%9D%B8%EC%95%B1-%EA%B2%B0%EC%A0%9C%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

[IOS] Apple(IOS)의 인앱 결제에 대해서 알아보자

이번 포스팅에서는 IOS 인앱 결제 관련 지침에 대해서 다뤄보겠습니다. IOS를 출시할 때 비지니스에 대해서 명확하게 정의가 되어있어야하고, 관련 기능 구현이 되어야 출시가 될 수 있습니다. 즉

minf.tistory.com

 

Flutter 인앱결제 관련 패키지는 아래와 같이 in_app_purchase를 선택하였습니다.

https://pub.dev/packages/in_app_purchase

 

in_app_purchase | Flutter package

A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play.

pub.dev

Flutter팀에서 만들기도 했고, 다른 패키지들보다 앱의 업데이트가 활발해서 해당 패키지로 선택하였습니다.

 

전체적인 프로세스는 아래와 같습니다.

https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase

- 결제 항목 생성 (항목에 대한 ID와 가격 등의 정보를 설정합니다.)
- 앱에서 Storekit을 통하여 App Store에 등록된 상품 불러옵니다.
- 받아온 상품에 대한 정보를 자체 서버로 보내줘서 거래한 transactionID를 보내주며 서버가 해당 결제가 진짜인지 가짜인지 검증 과정을 거쳐서 Client에게 결과를 보내줍니다.
- 검증 완료 후 App Store에 결제를 요청합니다.

 

위 과정을 패키지의 문서 및 애플 개발자 문서를 보면서 진행하겠습니다.

*인앱결제를 넣기 위해서는 사업자 등록 및 세금, 지불 정보 관리가 모두 되어있어야 결제 테스트가 가능합니다.

 

1. App Store Connect에 상품 등록

비지니스 설정

-1. App Store Connect -> 비지니스에서 세금 양식 및 은행계좌를 입력하고 ->유료 앱, 무료 앱 모두 활성화 시켜야합니다

*유료앱 계약이 안되어있으면 앱 코드가 정상적이여도 인앱결제 상품이 불러와지지 않습니다. 

준비물

- 사업자 등록 번호, 사업자 등록 (영문), 통신판매업 신호번호

 

ㅇ. 미국 세금 질문지

여기에서 보면 한국에서만 출시하더라도 애플 유료앱을 출시하려면 미국 세금 정보를 제출하는게 필수적인 과정의 일부라고 합니다.

이러한 요구사항은 미국 세금법에 따른 것으로, 미국 세금 당국(IRS)에 의해 규정되었다고 합니다. 

 

캡쳐를 못해서 텍스트로 대체하겠습니다.

- 질문 : 미국 세법상 거주자로 간주됩니까? || 답변 : 아니요

- 질문 : 미국 내 사업 활동을 하고 있습니까? || 답변 : 아니요

 

이렇게 하게 되면 아래와 같이 세금 양식을 작성하는 부분이 나오게 되는데 

 


먼저 U.S. Certificate of Foreign Status of Beneficial Owner를 보시겠습니다.

 

위 사진과 같이 Type of Beneficial Owner - Corporation,
Title - CEO를 입력해주시면 됍니다.


다음으로 U.S. Substitute Form W-8BEN-E를 알아보겠습니다.

U.S. Substitute Form W-8BEN-E는 미국 세무당국(IRS)이 사용하는 양식 중 하나이고, 미국 내 발생하는 세금 관련 정보를 제공하기위해 필요하다고 하네요.

 

아래와 같이 체크해주시고, 제출해주시면 됍니다.

 

 

 

이렇게 되면 미국 세금, 대한민국 세금 모두 활성화가 된 것을 확인하실 수 있습니다. 

 

 


마지막으로 디지털 서비스법에 관련 된 부분 인데

이 이슈는 DSA(Digital Services Act), EU(유럽연합) 27개국 전역에서 시행된 DMA에 따른 조치입니다. 

결론은 유럽에서 이용자들이 애플스토어가 아닌 다른 플랫폼에서도 앱을 다운받을 수 있게 하겠다는거고,

아이폰에 다른 앱스토어를 제공하는 개발자들은 애플의 승인을 받도록 하는 것 이라고 합니다.
그래서 우리가 생각해봐야할 것은

1. EU 나라에서 판매중인 앱들이 있는지?

2. How to know if you're trader? - 어떤 거래자인지 정보를 제공해줘야한다.

(2번은 조직이라면 DUNS, 주소, 이메일, 전화번호 || 개인이라면 주소 또는 사서함, 전화번호, 이메일 주소 가 자동으로 입력되어서 보내주게 된다.)

 

 

저는 EU나라에서 판매중인 앱들이 없으니 비거래자를 선택하고 확인을 눌렀습니다.

위 와 같이 잘 완료된 것을 확인하실 수 있습니다. 

이로써 비지니스에 대한 부분은 완료했습니다. 


인앱결제 상품 등록

- App Store -> 수익화 -> 앱 내 구입 등록

 

- 사용 가능 여부 설정

- 가격 변경 일정 - 앱 내 구입 가격 확인

- App Store 현지화

 

- 결과화면

2. 인앱 결제 테스트  (SandBox 테스터 계정 만들기 or TestFlight 이용)

결제 테스트를 하기 위해서는 2가지 방법이 있습니다.

첫번째는 SandBox 테스터 계정을 만들어서 인 앤 구매를 시도하거나,

두번째는 TestFlight에 초대를 받은 계정은 실제 Apple계정이지만 해당 계정으로 결제 테스트 진행 시 SandBox처럼 테스트 결제가 진행되어 어떠한 대금도 청구되지 않습니다.

 

저는 현재 TestFlight로 통해 앱을 받을 수 있어서 TestFlight로 진행해보겠지만 

SandBox 계정을 만들어서 SandBox로도 진행해보곘습니다.

 

ㅇ. SandBox계정으로 테스트

공식 문서를 참고하여 테스터 계정을 만들어줍니다.

-1. SandBox계정 만들기

App Store Connect -> 사용자 엑세스 -> Sandbox -> 테스트 계정 -> 테스트 계정 추가

계정을 만들어주실 때 Apple ID로 사용되거나 iTunes 또는 App Store에서 콘텐츠를 구입한 적 없는 이메일 주소를 사용하셔야합니다.

Apple ID로 하시면 아래와 같은 에러가 나올겁니다.

 

-2. SandBox 계정으로 App Store 로그인하기

AppStore -> 계정 -> 로그아웃 

 

설정 -> App store -> '샌드 박스 계정'을 선택하시면 됍니다. 

 

3. Xcode 설정 

- 1. Xcode로 예제 앱을 엽니다 File > Open File example/ios/Runner.xcworkspace.

-2. Tagets -> Signing & Capabilities -> + Capability 클릭 -> In-App Purchase 추가

 

 

4. 코드

 

ㅇ. 구매 내역 복원

구매 내역은 소모품 관련 내용은 나오지 않게 되고, 정기 결제 구독 내역만 나오게 됍니다.

// 구매 내역을 복원하는 메서드
  Future<void> restorePurchases() async {
    try {
      await _inAppPurchase.restorePurchases();
      // 복원된 구매 내역은 _handlePurchaseUpdates 메서드를 통해 _restoredPurchases 리스트에 추가됩니다.
    } catch (e) {
      // 복원 중 에러가 발생한 경우 처리
      print("Error restoring purchases: $e");
    }
  }

 

ㅇ. 상품 리스트 

아래 코드를 실행하면 각 상품의 리스트를 불러올 수 있습니다.

  List<ProductDetails> _products = []; // 사용 가능한 상품 목록을 저장합니다.

  // 인앱 결제 프로세스 시작
  final bool available = await _inAppPurchase.isAvailable();
  if (!available) {
    // 인앱 결제를 사용할 수 없는 경우
    showErrorDialog(context, "인앱 결제를 사용할 수 없습니다.");
    return;
  }

  // 상품 정보 요청
  const Set<String> kIds = {_productId};
  final ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(kIds);
  if (response.notFoundIDs.isNotEmpty) {
    // 상품 정보를 찾을 수 없는 경우
    showErrorDialog(context, "상품 정보를 찾을 수 없습니다.");
    return;
  }

  // 상품 정보 로그 출력
  for (final product in response.productDetails) {
    print('상품 ID: ${product.id}');
    print('상품 제목: ${product.title}');
    print('상품 설명: ${product.description}');
    print('가격: ${product.price}');
    print('통화 코드currencyCode : ${product.currencyCode}');
    print('원시 가격 문자열: ${product.rawPrice}');
  }


    setState(() {
      _products = response.productDetails; // 상품 정보 업데이트
    });

 

ㅇ. 구매 업데이트 리스너 추가 (initState에 넣어줍니다.)

아래 코드를 적용하면 구매가 이루어질때마다 상태값을 추적할 수 있습니다.

 

class _MyAppState extends State<MyApp> {
  StreamSubscription<List<PurchaseDetails>> _subscription;

  @override
  void initState() {
    final Stream purchaseUpdated =
        InAppPurchase.instance.purchaseStream;
    _subscription = purchaseUpdated.listen((purchaseDetailsList) {
      _listenToPurchaseUpdated(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (error) {
      // handle error here.
    });
    super.initState();
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
  
// 구매 업데이트를 처리하는 방법의 예는 다음과 같습니다.

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);
      }
    }
  });
}

 

위 코드에서 purchaseDetails.status 상태값에 대해서 말씀드리겠습니다.

개발을 하실 때 상태값에 따른 예외처리를 해주시면 될 것 같습니다.

purchased: 구매가 성공적으로 완료되었습니다. 사용자가 상품에 대한 결제를 완료하고, 해당 상품을 이용할 수 있게 됩니다. 애플리케이션은 이 상태를 확인한 후 사용자에게 상품을 제공하고, 필요한 경우 서버에 구매 정보를 전송하여 추가 검증을 수행할 수 있습니다.

pending: 구매가 보류 중입니다. 일부 결제 방식에서는 결제 승인을 위해 추가적인 사용자 상호작용이나 처리 시간이 필요할 수 있습니다. 애플리케이션은 사용자에게 결제가 진행 중임을 알리고, 최종 결제 상태를 기다려야 합니다.

canceled: 사용자가 구매를 취소했습니다. 결제 프로세스 중에 사용자가 구매를 중단하거나, 결제 시스템에서 구매 요청을 취소한 경우입니다. 애플리케이션은 사용자에게 구매 취소를 알리고, 필요한 경우 추가적인 사용자 인터페이스나 처리를 제공할 수 있습니다.

error: 구매 과정에서 오류가 발생했습니다. 네트워크 문제, 결제 시스템의 오류, 구성 문제 등 다양한 원인으로 인해 구매가 실패할 수 있습니다. 애플리케이션은 사용자에게 오류 발생을 알리고, 가능한 경우 다시 시도하도록 유도할 수 있습니다.

restored: 이전에 구매한 상품이 복원되었습니다. 주로 구독형 서비스나 비소모성 상품을 다시 설치한 경우, 사용자가 이전에 구매한 상품을 다시 얻기 위해 복원 과정을 진행할 때 사용됩니다.

 

 

5. 결과화면


참고

https://github.com/flutter/packages/blob/main/packages/in_app_purchase/in_app_purchase/example/README.md

https://developer.apple.com/kr/help/app-store-connect/test-in-app-purchases/overview-of-testing-in-sandbox/

 

sandbox에서의 테스트 개요 - App Store Connect - 도움말 - Apple Developer

포럼 Apple 엔지니어 및 다른 개발자에게 개발 주제에 관해 질문하고 이야기를 나눌 수 있습니다. 포럼 보기(영문)

developer.apple.com

https://guide.ureca.im/setting/apple/payment-contract

Comments