개요
Flutter 환경에서 구현된 어플리케이션에서 원스토어 결제 라이브러리의 최신 기능 제공합니다. 이 가이드는 원스토어 결제 라이브러리 기능을 Flutter 환경에서 적용하는 방법을 설명합니다.
개발 버전
Purchase: v21.02.01
App License Checker: v2.2.1
플러그인 설정
pubspec.yaml 파일에 플러그인 추가하기
flutter pub get
을 통하여 패키지를 다운로드합니다.
Copy dependencids:
flutter_onestore_inapp: ^0.3.0
Android build.gradle에 종속성 추가하기
Top-Level build.gradle 파일에 maven repository 주소 추가하기
Copy allprojects {
repositories {
maven { url 'https://repo.onestore.net/repository/onestore-sdk-public' }
}
}
AndroidMainifest.xml 파일에 <queries> 추가하기
<manifest>
태그의 직속 하위에 위치하며, 아래와 같은 요소를 추가해야 합니다.
Copy <manifest>
<queries>
<intent>
<action android:name="com.onestore.ipc.iap.IapService.ACTION" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="onestore" />
</intent>
</queries>
<application>
</application>
</manifest>
스토어 지정을 위한 개발자 옵션 설정
v21.02.00 업데이트 – 글로벌 스토어 선택 기능 추가
IAP SDK 21.02.00 부터 아래와 같이 onestore:dev_option
의 android:value
값을 설정하면, SDK와 연동되는 스토어 앱을 지정 할 수 있습니다.
<application>
태그 직속 하위에 위치하며, 아래와 같은 요소를 추가합니다.
Copy <manifest>
<application>
<activity>
</activity>
<meta-data android:name="onestore:dev_option" android:value="onestore_01" />
</application>
</manifest>
값 (android:value)
적용 대상 국가/지역
싱가포르, 타이완 (Singapore, Taiwan)
미국 – Digital Turbine (United States)
21.01.00 버전에서는 android:value 값이 global만 설정 가능하며, 싱가포르/타이완 스토어 앱만 지정이 가능했습니다.
주의 : 배포 버전의 바이너리에서는 이 옵션을 반드시 제거해주세요.
앱에서 원스토어 인앱 결제 라이브러리 적용하기
로그 레벨 설정
개발 단계에서 로그 레벨을 설정하여 SDK의 데이터 흐름을 좀 더 자세히 노출할 수 있습니다. android.util.Log
에 정의된 값을 기반으로 동작합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
// 앱 개발 시 필요에 의해 SDK & Plugin의 로그 레벨을 변경하면 좀 더 자세한 정보를 얻을 수 있습니다.
// WARNING! Release Build 시엔 로그 레벨 세팅을 제거 바랍니다. (default: Level.info)
OneStoreLogger.setLogLevel(LogLevel.verbose);
}
}
배포 빌드 버전에서는 보안에 취약할 수 있으니 이 옵션을 삭제 해야 합니다.
로그인 요청하기
원스토어 인앱 결제는 로그인 기반으로 구동되는 서비스입니다. 앱 최초 시작 시 구매 라이브러리의 API 호출하기 전에 로그인을 유도합니다. 구매 라이브러리 요청 시 토큰 만료나 다른 여러 가지 사항을 미연에 방지할 수 있습니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
final OneStoreAuthClient _authClient = OneStoreAuthClient();
Future<void> launchSignInFlow() async {
await _authClient.launchSignInFlow().then((signInResult) {
if (signInResult.isSuccess()) {
// success
} else {
// failure
}
});
}
구매 라이브러리 초기화
PurchaseClientManager
인스턴스 초기화를 요청합니다. 이때 필요한 값은 public license key입니다. 이 라이선스 키는 원스토어 개발자 센터에 앱 등록을 완료하면 발급받을 수 있습니다.
Copy
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
_clientManager.initialize("your license key");
public license key는 SDK 내에서 구매 응답에 대한 위변조 체크에 사용됩니다.
구매 데이터 업데이트 및 오류 응답 청취하기
PurchaseClientManager.purchasesUpdatedStream
을 통해 구매 완료 응답을 받을 준비를 합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
late StreamSubscription<List<PurchaseData>> _purchaseDataStream;
MyPurchaseManager() {
_clientManager.initialize("your license key");
// 구매 완료 후 Stream을 통해 데이터가 전달됩니다.
_purchaseDataStream = _clientManager.purchasesUpdatedStream.listen(
(List<PurchaseData> purchasesList) {
_listenToPurchasesUpdated(purchasesList);
}, onError: (error) {
// 구매가 실패 되었거나 유저가 취소가 되었을 때 응답 됩니다.
_logger.d('purchaseStream error: $error');
}, onDone: () {
_purchaseDataStream.cancel();
});
}
void _listenToPurchasesUpdated(List<PurchaseData> purchasesList) {
// do something
}
}
상품 상세 정보 조회하기
PurchaseClientManager.queryProductDetails
API를 통해 원스토어 개발자 센터에 등록된 인앱 상품의 상세 정보를 조회할 수 있습니다.
상품 상세 정보는 ProductDetail
객체를 담은 리스트로 전달됩니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
static const consumableIds = ['product_1', 'product_2'];
static const subscriptionIds = ['week', 'month', 'three_month'];
final List<ProductDetail> _products = [];
Future<void> fetchProductDetails() async {
var responses = await Future.wait(<Future<ProductDetailsResponse>>[
_clientManager.queryProductDetails(
productIds: consumableIds,
productType: ProductType.inapp
),
_clientManager.queryProductDetails(
productIds: subscriptionIds,
productType: ProductType.subs
)
]);
if (responses.first.iapResult.isSuccess()) {
final List<ProductDetail> result =
responses.expand((element) => element.productDetailsList).toList();
_products.clear();
_products.addAll(result);
notifyListeners();
} else {
_handleError('fetchProductDetails', responses.first.iapResult);
}
}
}
요청하는 상품 리스트가 적다면 위의 예제처럼 상품 타입에 따라 개별 요청이 아닌 ProductType.all
타입으로 요청할 수 있습니다.
Copy class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
static const consumableIds = ['product_1', 'product_2'];
static const subscriptionIds = ['week', 'month', 'three_month'];
final List<ProductDetail> _products = [];
Future<void> fetchProductDetails() async {
var productDetailsResponse = await _clientManager.queryProductDetails(
productIds: (consumableIds + subscriptionIds),
productType: ProductType.all
);
if (productDetailsResponse.iapResult.isSuccess()) {
final List<ProductDetail> result = productDetailsResponse.productDetailsList;
_products.clear();
_products.addAll(result);
} else {
_handleError('fetchProductDetails', productDetailsResponse.iapResult);
}
}
}
등록된 인앱상품의 개수가 많은 경우 응답 지연이 발생할 수 있습니다. 이런 경우 안정성이나 속도 측면에서 400개 단위로 호출 하시는 것을 권장 드립니다.
ProductType.all
타입은 상품 상세 조회하기에서만 사용할 수 있는 옵션입니다. 다른 API에서는 사용할 수 없습니다.
구매 요청하기
PurchaseClientManager.launchPurchaseFlow()
API를 사용하여 구매 요청합니다.
수량 (default: 1) 복수 구매 시 max: 10
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<IapResult> launchPurchaseFlow(ProductDetail productDetail,
int? quantity, String? developerPayload) async {
return await _clientManager.launchPurchaseFlow(
productDetail: productDetail,
quantity: quantity,
developerPayload: developerPayload
);
}
}
정기 결제 업그레이드 또는 다운그레이드
정기 결제는 취소될 때까지 자동으로 갱신됩니다. 정기 결제는 다음 상태를 가질 수 있습니다.
활성 : 사용자가 콘텐츠 사용에 문제가 없는 양호한 상태이며 정기 결제에 접근할 수 있습니다.
일시 중지 예약 : 사용자가 정기 결제를 이용 중 일시 중지를 하고 싶을 때 선택할 수 있습니다.
주간 정기 결제: 1~3주 단위로 일시 중지할 수 있습니다.
월간 정기 결제: 1~3개월 단위로 일시 중지할 수 있습니다.
연간 정기 결제: 일시 중지를 지원하지 않습니다.
해지 예약 : 사용자가 정기 결제를 이용 중이지만 취소하고 싶을 때 선택할 수 있습니다. 다음 결제일에 결제가 되지 않습니다.
유예, 보류 : 사용자에게 결제 문제가 발생하면 다음 결제일에 결제가 되지 않습니다. 취소 예약을 할 수 없으며 즉시 "구독 해지"를 할 수 있습니다.
정기 결제를 업데이트하기 위해서는 필수로 비례 배분 모드
를 적용해야 합니다. 아래는 비례 배분 모드
별 설명입니다.
IMMEDIATE_WITH_TIME_PRORATION
정기 결제의 교체가 즉시 이루어지며, 남은 시간은 가격 차이를 기반으로 조정되어 입금되거나 청구됩니다. (이것은 기본 동작입니다.)
IMMEDIATE_AND_CHARGE_PRORATED_PRICE
정기 결제의 교체가 즉시 이루어지며, 청구 주기는 동일하게 유지됩니다. 나머지 기간에 대한 가격이 청구됩니다. (이 옵션은 업그레이드에서만 사용할 수 있습니다.)
IMMEDIATE_WITHOUT_PRORATION
정기 결제의 교체가 즉시 이루어지며, 다음 결제일에 새로운 가격이 청구됩니다. 청구 주기는 동일하게 적용됩니다.
기존 요금제가 만료되면 교체가 적용되며 새 요금이 동시에 청구됩니다.
PurchaseClientManager.launchUpdateSubscription()
API를 통해 요청할 수 있습니다.
정기결제는 구매 요청하기 와 동일한 API를 사용하여 사용자에게 업그레이드 또는 다운그레이드를 제공할 수 있습니다. 다만, 정기 결제의 업그레이드 다운그레이드를 적용하기 위해선 기존 정기 결제 구매 토큰과 비례 배분 모드 값이 필수로 필요합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<IapResult> launchUpdateSubscription(ProductDetail productDetail,
PurchaseData oldPurchaseData, ProrationMode prorationMode) async {
return await _clientManager.launchUpdateSubscription(
productDetail: productDetail,
oldPurchaseData: oldPurchaseData,
prorationMode: prorationMode
);
}
}
구매 후 처리
PurchaseClientManager.launchPurchaseFlow()
PurchaseClientManager.launchUpdateSubscription()
API를 사용하여 구매가 성공적으로 이루어졌다면 구매 데이터 업데이트 및 오류 응답 청취하기 서 등록한 _listenToPurchasesUpdated()
를 통해 응답을 받을 수 있습니다.
성공적으로 구매 완료되어 응답을 받았다면, 사용자는 소비(Consume) 또는 확인(Acknowledge) 작업을 하는 것이 매우 중요합니다.
3일 이내에 구매를 확인(acknowledge ) 또는 소비(consume )를 하지 않으면 사용자에게 상품이 지급되지 않았다고 판단되어 자동으로 환불됩니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
final List<ProductDetail> _products = [];
// 상품 상세 정보에서 ProductType.inapp인 것만 필터링 된 데이터
List<ProductDetail> get consumableProducts => _products
.where((element) => element.productType == ProductType.inapp)
.toList();
// 상품 상세 정보에서 ProductType.subs인 것만 필터링 된 데이터
List<ProductDetail> get subscriptionProducts => _products
.where((element) => element.productType == ProductType.subs)
.toList();
void _listenToPurchasesUpdated(List<PurchaseData> purchasesList) {
if (purchasesList.isNotEmpty) {
for (var element in purchasesList) {
if (consumableProducts.any((p) => p.productId == element.productId)) {
/// [ProductType.inapp] 상품은 [consumePurchase] 호출하여 소비합니다.
} else if (subscriptionProducts.any((p) => p.productId == element.productId)) {
/// [ProductType.subs] 상품은 [acknowledgePurchase] 호출하여 확인합니다.
}
}
}
}
}
소비하기 (Consume)
PurchaseClientManager.consumePurchase()
API를 사용하여 관리형 상품(ProductType.inapp
)을 소비합니다.
관리형 상품(ProductType.inapp
)은 소비를 하지 않으면 재 구매가 불가합니다.
관리형 상품(ProductType.inapp
)은 API 사용에 따라 두 가지로 사용될 수 있습니다.
소모성 상품: 구매 요청 → 응답 → 아이템 지급 → consumePurchase
기간제 상품: 구매 요청 → 응답 → 아이템 지급 → acknowledgePurchase
→ 일정 기간이 지난 후 → consumePurchase
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<void> consumePurchase(PurchaseData purchaseData) async {
await _clientManager
.consumePurchase(purchaseData: purchaseData)
.then((iapResult) {
// IapResult를 통해 해당 API의 성공 여부를 판단할 수 있습니다.
if (iapResult.isSuccess()) {
fetchPurchases([ProductType.inapp]);
}
});
}
}
확인하기 (Acknowledge)
PurchaseClientManager.acknowledgePurchase()
API를 사용하여 관리형 상품(ProductType.inapp
) 또는 구독형 상품(ProductType.subs
)의 확인을 요청합니다.
구독형 상품(ProductType.subs
)는 소비(Consume)는 할 수 없고 확인(Acknowledge)만 가능합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<void> acknowledgePurchase(PurchaseData purchaseData) async {
await _clientManager
.acknowledgePurchase(purchaseData: purchaseData)
.then((iapResult) {
// IapResult를 통해 해당 API의 성공 여부를 판단할 수 있습니다.
if (iapResult.isSuccess()) {
fetchPurchases([ProductType.subs]);
}
});
}
}
확인 작업이 완료되었다면 구독형 상품(ProductType.subs
)의 경우 구매 내역 조회하기 를 통해 PurchaseData
업데이트해야 PurchaseData.isAcknowledged
값이 변경된 것을 확인할 수 있습니다.
구매 내역 조회하기
PurchaseClientManager.queryPurchases()
API를 사용하여 소비되지 않은 구매 내역을 요청합니다.
구매 내역 조회하기의 응답 데이터는 구매 데이터 업데이트 및 오류 응답 청취하기 통해 받는 데이터와 동일합니다.
구매 완료 후 데이터를 처리하는 것만으로는 앱이 모든 구매를 처리하는 것을 보장하기에 충분하지 않습니다. 앱에서 사용자가 구매한 모든 항목을 인식하지 못할 수 있습니다. 앱에서 구매 추적을 놓치거나 구매를 인식하지 못할 수 있는 몇 가지 시나리오는 다음과 같습니다.
구매 중 네트워크 문제 : 사용자가 구매를 성공적으로 완료하고 원스토어에서 확인을 받았지만 기기가 구매 알림을 받기 전에 네트워크 연결이 끊어졌을 경우
여러 기기 : 사용자는 한 기기에서 항목을 구입한 후 기기를 전환할 때 이 항목이 표시되기를 기대합니다.
이러한 상황에 대처하려면 구매 내역 조회하기 API를 상황에 맞게 호출해야 합니다.
어플리케이션이 백그라운드에서 포그라운드로 재 진입했을 경우
어플리케이션의 상황에 맞게 사용해 주세요.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
class MyPurchaseManager {
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<void> fetchPurchases(ProductType type) async {
await _clientManager
.queryPurchases(productType: type)
.then((response) {
if (response.iapResult.isSuccess()) {
if (type == ProductType.inapp) {
for (var purchaseData in response.purchasesList) {
consumePurchase(purchaseData);
}
} else if (type == ProductType.subs) {
for (var purchaseData in response.purchasesList) {
if (!purchaseData.isAcknowledged) {
acknowledgePurchase(purchaseData);
}
}
}
} else {
_handleError('fetchPurchases($type)', response.iapResult);
}
});
}
}
정기 결제 관리 화면 열기
PurchaseClientManager.launchManageSubscription()
API를 사용하여 구독 상품의 상세 페이지로 이동합니다.
구독 상품의 설정 변경은 유저의 몫으로 관리 메뉴에서 할 수 있는 것들은 아래와 같습니다.
PurchaseData
값이 null일 경우 특정 구독 상품의 상세 페이지가 아닌 정기 결제 리스트 화면으로 이동합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<void> launchManageSubscription(PurchaseData? purchaseData) async {
await _clientManager.launchManageSubscription(purchaseData);
}
StoreEnvironment API 기능 추가
StoreEnvironment.getStoreType()
API는 SDK가 탑재된 애플리케이션이 원스토어를 통해 설치되었는지를 판단하는 기능을 제공합니다.
Store Type 정의
해당 API는 StoreType
을 반환하며, 아래 네 가지 값 중 하나를 가집니다.
StoreType
value
description
앱 설치 스토어 정보를 알 수 없음 (APK 직접 설치, 출처 불명 등)
ONE Store에서 설치됨 (또는 개발자 옵션이 활성화된 경우)
API 사용 방법
해당 API는 StoreEnvironment.getStoreType()
을 호출하여 사용할 수 있습니다.
Copy StoreType storeType = await OneStoreEnvironment.getStoreType();
switch (storeType) {
case StoreType.unknown:
print("스토어 정보를 알 수 없습니다.");
break;
case StoreType.oneStore:
print("ONE Store에서 설치된 앱입니다.");
break;
case StoreType.vending:
print("Google Play Store에서 설치된 앱입니다.");
break;
case StoreType.etc:
print("기타 스토어에서 설치된 앱입니다.");
break;
}
스토어 판단 기준
이 API는 세 가지 방법을 통해 설치된 스토어를 판별합니다.
원스토어 마켓 서명을 통해 배포된 경우
원스토어의 마켓 서명을 통한 배포 여부를 확인하여, 원스토어에서 설치된 앱인지 확인합니다.
Installer Package Name을 기반으로 판별
원스토어의 마켓 서명을 통해 배포되지 않은 경우, PackageManager.getInstallerPackageName() API를 이용하여 앱 설치 시 사용된 스토어 정보를 확인합니다.
개발자 옵션(onestore:dev_option
)이 활성화된 경우
onestore:dev_option 이 설정 되어있으면 무조건 StoreType.ONESTORE로 응답합니다.
활용 예시
스토어별 UI 차별화 적용
원스토어와 다른 앱 마켓에서 제공하는 결제 시스템이 다를 경우, UI를 다르게 설정할 수 있습니다.
Copy if (await OneStoreEnvironment.getStoreType() == StoreType.oneStore) {
showOneStorePaymentUI()
} else {
showDefaultPaymentUI()
}
스토어별 기능 차단
특정 기능을 원스토어에서만 사용하도록 설정할 수 있습니다.
Copy if (await OneStoreEnvironment.getStoreType() != StoreType.oneStore) {
print("이 기능은 ONE Store에서만 사용할 수 있습니다.");
}
enableOneStoreExclusiveFeature()
원스토어 서비스 설치하기
PurchaseClientManager.launchUpdateOrInstall()
API를 호출하여 '원스토어 서비스 앱'을 설치할 수 있습니다.
PurchaseClientManager
API를 사용하는 중에 에러 응답 중 RESULT_NEED_UPDATE
코드가 발생했을 경우가 있습니다. 이는 원스토어 서비스 앱이 설치되지 않았거나 In-app SDK에서 요구하는 버전보다 낮을 경우 발생합니다.
Copy import 'package:flutter_onestore_inapp/flutter_onestore_inapp.dart';
final PurchaseClientManager _clientManager = PurchaseClientManager.instance;
Future<void> launchUpdateOfInstall() async {
await _clientManager.launchUpdateOrInstall().then((iapResult) {
if (iapResult.isSuccess()) {
fetchPurchases();
fetchProductDetails();
}
});
}