구매 흐름 재설계
재설계 이유
1. 지불을 요청할 때 하나 이상의 응답이 있을 수 있기 때문
2. 구매는 세션간 비동기 작업이므로 요청을 완료하는 데 시간이 필요하고, 앱을 종료하거나 충돌이 일어난 경우에도 계속 동작할 수 있다
3. 구매가 보류중이면 수행된 작업을 추적하기 어려울 수 있다
4. 청구 흐름은 event 패턴이 아닌 callback 패턴
getProducts()
를 호출 후 유효한 응답이 있다면
requestPurchase()
를 호출할 수 있다
구독 가능 상품은 소모품과 동일하게 구매 가능, 사용자는 iOS 시스템 설정으로 구독 취소 가능
구매 요청을 (purchase) 보내기 전에
react-native-iap의 purchaseUpdatedListener 를 설정해야 한다
애플리케이션이 실행될 때 즉시 업데이트 수신을 시작하는 것이 좋다
앱이 종료된 동안 완료되었거나 네트워크 오류 또는 버그로 인해 확인에 실패한 경우에도 구매에 성공할 수 있다
import {
initConnection,
purchaseErrorListener,
purchaseUpdatedListener,
type ProductPurchase,
type PurchaseError,
flushFailedPurchasesCachedAsPendingAndroid,
} from 'react-native-iap';
const App = () => {
let purchaseUpdateSubscription = null;
let purchaseErrorSubscription = null;
useEffect(() => {
initConnection().then(() => {
// we make sure that "ghost" pending payment are removed
// (ghost = failed pending payment that are still marked as pending in Google's native Vending module cache)
flushFailedPurchasesCachedAsPendingAndroid()
.catch(() => {
// exception can happen here if:
// - there are pending purchases that are still pending (we can't consume a pending purchase)
// in any case, you might not want to do anything special with the error
})
.then(() => {
purchaseUpdateSubscription = purchaseUpdatedListener(
(purchase: SubscriptionPurchase | ProductPurchase) => {
console.log('purchaseUpdatedListener', purchase);
const receipt = purchase.transactionReceipt;
if (receipt) {
yourAPI
.deliverOrDownloadFancyInAppPurchase(
purchase.transactionReceipt,
)
.then(async (deliveryResult) => {
if (isSuccess(deliveryResult)) {
// Tell the store that you have delivered what has been paid for.
// Failure to do this will result in the purchase being refunded on Android and
// the purchase event will reappear on every relaunch of the app until you succeed
// in doing the below. It will also be impossible for the user to purchase consumables
// again until you do this.
// If consumable (can be purchased again)
await finishTransaction({purchase, isConsumable: true});
// If not consumable
await finishTransaction({purchase, isConsumable: false});
} else {
// Retry / conclude the purchase is fraudulent, etc...
}
});
}
},
);
purchaseErrorSubscription = purchaseErrorListener(
(error: PurchaseError) => {
console.warn('purchaseErrorListener', error);
},
);
});
});
}, [])
return () => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
}
}
}
}
아래와 같이 메소드를 정의하고 사용자가 버튼을 누를 때 호출
const App = () => {
let requestPurchase;
let requestSubscription;
requestPurchase = async (sku: string) => {
try {
await requestPurchase({
sku,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
});
} catch (err) {
console.warn(err.code, err.message);
}
};
requestSubscription = async (sku: string, offerToken: string?) => {
try {
await requestSubscription(
{sku},
...(offerToken && {subscriptionOffers: [{sku, offerToken}]}),
);
} catch (err) {
console.warn(err.code, err.message);
}
};
/**
* For one-time products
*/
render() {
return (
<Pressable onPress={() => requestPurchase(product.productId)}>
{/* ... */}
</Pressable>
);
}
/**
* For subscriptions products
*/
render() {
if (Platform.OS == 'android') {
return product.subscriptionOfferDetails.map((offer) => (
<Pressable
onPress={() =>
requestSubscription(product.productId, offer.offerToken)
}
>
{/* ... */}
</Pressable>
));
} else {
return (
<Pressable
onPress={() => requestSubscription(product.productId, null)}
>
{/* ... */}
</Pressable>
);
}
}
}
/// 작성 예정
New Purchase Flow 새로운 구매 플로우
대부분의 경우 "스토어 키트 흐름"을 처리해야 합니다.[2][apple-store-kit-flow]는 예를 들어 신용카드 정보가 만료되었을 때 사용자가 계정 문제를 해결한 후 성공적으로 결제할 때 발생합니다.
위와 같은 이유로 우리는 buy product를 제거하고 requestPurchase를 사용하기로 결정했습니다. purchaseUpdateListener는 성공적인 구매를 수신하고 purchaseErrorListener는 구매 시도 중 발생한 모든 실패 결과를 수신합니다.
구매는 구매를 완료할 때까지 모든 앱을 다시 시작할 때마다 업데이트된 청취자에게 계속 전송됩니다.
모든 구매는 finishTransaction()을 호출하여 소비해야 합니다.