5.0.1
0.63.3
IOS
purchaseUpdatedListener๊ฐ ์ฌ๋ฌ ๋ฒ ํธ์ถ๋์ง ์์
์์์ ์ฌ๋ฌ ๋ฒ, ๋๋ก๋ ์ค๊ฐ์ ํธ์ถ๋๋ purchaseUpdatedListener
์ค์ ์ฅ์น (iOS 14, 13 in Test Flight ๋ฐ AppStore)
(๋ค๋น๊ฒ์ดํฐ ๊ตฌ์ฑ ์์)
const itemSkus = ['01', '02'];
const processNewPurchase = async (purchase) => {
const { productId, transactionReceipt } = purchase;
if (transactionReceipt !== undefined && transactionReceipt) {
//backend call with fetch - validating receipt
if (data.ack === 'success') {
console.log('finished');
await finishTransaction(purchase);
} else if (data.ack === 'failure') {
props.setProcessing(false);
console.log('error');
}
}
};
const getProductsIAP = useCallback(async () => {
await clearProductsIOS();
await clearTransactionIOS();
try {
const result = await initConnection();
const products = await getProducts(itemSkus);
props.setProducts(products);
console.log('result', result);
} catch (err) {
console.warn(err.code, err.message);
}
purchaseUpdateSubscription = purchaseUpdatedListener(
async (purchase) => {
const receipt = purchase.transactionReceipt;
console.log('purchaseUpdatedListener');
if (receipt) {
try {
await processNewPurchase(purchase);
} catch (ackErr) {
console.log('ackErr', ackErr);
}
} else {
console.log('purchaseUpdatedListener error: receipt');
}
},
);
purchaseErrorSubscription = purchaseErrorListener(
(error: PurchaseError) => {
console.log('purchaseErrorListener', error);
console.log(JSON.stringify(error));
},
);
setLoading(false);
}, []);
useEffect(() => {
getProductsIAP();
return () => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
purchaseUpdateSubscription = null;
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
purchaseErrorSubscription = null;
}
};
}, []);
์ด๋ฒคํธ๊ฐ ๋์ผํ ๊ตฌ๋งค ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ํฉ๋๊น?
์, ์ฑ๊ณต์ ์ธ ๊ฒฐ๊ณผ / ์์์ฆ๊ณผ ํจ๊ป ์คํ๋ฉ๋๋ค. ํ์ฌ ์์์ฆ์ด ์์ผ๋ฉด ์คํ๋์ง๋ง ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฒ์์๋ ์คํจํฉ๋๋ค.
๊ตฌ์ฑ ์์๊ฐ ๋ค์ ๋ ๋๋ง๋์ง ์์๋์ง ํ์ธํด ์ฃผ์๊ฒ ์ต๋๊น? ๊ตฌ์ฑ ์์๊ฐ ๋ค์ ๋ ๋๋ง๋๊ณ ๋ฆฌ์ค๋๊ฐ ๋ช ๋ฒ์ ๊ฑธ์ณ ์์๋ ๋ ๋ง์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ด๊ฑด ํ๋ค๊ฒ ๊ณต๊ฐ ๋๊ฒ ์ง๋ง ์ฌ๊ฑด์ ์ฌํํด์ผํฉ๋๋ค.
์, "NavigatorContainer"๋ผ๋ ๊ฒ์ ์ ๊ฐ ๋ค๋ฅธ ์ํ๋ฅผ ์ค์ ํ๊ณ ํญ๋ชฉ์ ์
๋ฐ์ดํธํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. ๊ทธ๋ฌ๋
๊ทธ๋ฌ๋ ๊ทธ๊ฒ์ ์ฒญ์ทจ์ ๊ตฌ๋ ์ ์ทจ์ํ๊ฑฐ๋ ์ด๊ฒ์ ํผํ๋ ๊ฒ์ ๋๋ค.
useEffect(() => {
getProductsIAP();
return () => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
purchaseUpdateSubscription = null;
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
purchaseErrorSubscription = null;
}
};
}, []);
์, "NavigatorContainer"๋ผ๋ ๊ฒ์ ์ ๊ฐ ๋ค๋ฅธ ์ํ๋ฅผ ์ค์ ํ๊ณ ํญ๋ชฉ์ ์ ๋ฐ์ดํธํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. ๊ทธ๋ฌ๋ ํ๋๋ง ๋ ๋๋ง ํ ๋ค์ ๋ค๋ฅธ ์ํ, ์ํ์ ์ ๋ฐ์ดํธ๋ฅผ ์ํด "๋ ๋๋ง"ํฉ๋๋ค.
๊ทธ๋ฌ๋ ๊ทธ๊ฒ์ ์ฒญ์ทจ์ ๊ตฌ๋ ์ ์ทจ์ํ๊ฑฐ๋ ์ด๊ฒ์ ํผํ๋ ๊ฒ์ ๋๋ค.
useEffect(() => { getProductsIAP(); return () => { if (purchaseUpdateSubscription) { purchaseUpdateSubscription.remove(); purchaseUpdateSubscription = null; } if (purchaseErrorSubscription) { purchaseErrorSubscription.remove(); purchaseErrorSubscription = null; } }; }, []);
๋ค, ์ด๊ฒ๋ ์ธ๊ธํ์ต๋๋ค. ๋ด๊ฐ ๊ธฐ์ตํ๋ ์ด์ ํธ๋ฅผ ๊ณต์ ํ์ต๋๋ค. ์ฐ๋ฆฌ๋ ๋ฒ์์ด ํ์ํฉ๋๋ค.
๋ํ ๊ฑฐ๋๋ฅผ ์ ๋ฆฌํ๊ฑฐ๋ ๋ค๋ฅธ testflight ๊ณ์ ์ผ๋ก ์๋ํ๋ ๊ฒ์ด ๋์์ด ๋ ์ ์์ต๋๋ค.
const getProductsIAP = useCallback(async () => {
await clearProductsIOS();
await clearTransactionIOS();
.....
TestFlight, ํ๋ก๋์ ๋ฐ ๋ค์ํ ์ฅ์น์์ ํ ์คํธ๋์์ต๋๋ค.
๋ค๋ฅธ ๊ณ์ ?
๋ค, ๊ฐ์ ๊ฒฐ๊ณผ์ ๋๋ค.
์ด์ํ. IapExample
ํ๋ก์ ํธ์์ ์ฝ๋ ์ค ๋ํซ์ ์ฌ์ฉํด ๋ณผ ์ ์์ต๋๊น?
๋๋ ๋ค์๊ณผ ๊ฐ์ ํ๋์ํ๋ค.
1) ์ฌ์ฉ์๊ฐ IAP๋ฅผ ๊ตฌ๋งคํฉ๋๋ค.
2) purchaseUpdatedListener๊ฐ ํธ์ถ๋ฉ๋๋ค.
3) ์ฌ์ฉ์๊ฐ ์ฑ์ ์ ๊ฑฐํฉ๋๋ค.
4) ์ฌ์ฉ์๊ฐ ์ฑ์ผ๋ก ๋์์ต๋๋ค.
5) purchaseUpdatedListener๊ฐ ํธ๋ฆฌ๊ฑฐ๋๊ณ ๋ฃจํ๊ฐ ์งํ๋ฉ๋๋ค.
์์ ์ ์ฌ์ฉํ๊ณ ์์ง๋ง ๋ค์ ๋ ๋๋ง๋์ง ์์ต๋๋ค.
๋๋ "react-native-iap": "4.4.1"๋ฐ "react-native": "0.63.3"์ ์์ต๋๋ค.
5.0.0์ผ๋ก ์ ๊ทธ๋ ์ด๋ํ๋ ค๊ณ ํ์ง๋ง ๋์ผํ ๋์์ด ๋ํ๋ฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ ๋์์ด ์์ต๋๋ค.
- ์ฌ์ฉ์๊ฐ IAP๋ฅผ ๊ตฌ๋งคํฉ๋๋ค.
- PurchaseUpdatedListener๊ฐ ํธ์ถ๋ฉ๋๋ค.
- ์ฌ์ฉ์๊ฐ ์์ฉ ํ๋ก๊ทธ๋จ์ ์ ๊ฑฐํฉ๋๋ค.
- ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋์๊ฐ๋๋ค.
- purchaseUpdatedListener๊ฐ ๋ฐ์ํ๊ณ ๋ฃจํ๋ฅผ ํ์ฑํฉ๋๋ค.
์์ ์ ์ฌ์ฉํ๊ณ ์์ง๋ง ๋ฐ๋ณต๋๋ ๋ด์ฉ์ด ์์ต๋๋ค.
๋๋ "react-native-iap": "4.4.1"๋ฐ "react-native": "0.63.3"์ ์์ต๋๋ค.
5.0.0์ผ๋ก ์ ๋ฐ์ดํธํ๋ ค๊ณ ํ์ง๋ง ๋์ผํ ๋์์ด ๋ํ๋ฉ๋๋ค.
์๋ ํ์ธ์. ์ง๋ฌธ ํ๋. ์ด๊ฒ์ ๋ํ ํด๊ฒฐ์ฑ ์ ์ฐพ์์ต๋๊น?
์ฌ์ ํ ๊ฐ์ ๋ฌธ์ ์ ๋๋ค. purchaseUpdatedListener๊ฐ ์์๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ต๋ํ ๋นจ๋ฆฌ ์์ ํ์ญ์์ค. ํด๊ฒฐ์ฑ ์ด ์์ต๋๋ค.
์ด๊ฒ๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ช ๋ฌ ๋์ ๊ณ ์ ๋์ง ์๋ ๊ฒ์ด ์ด๋ป๊ฒ ๊ฐ๋ฅํฉ๋๊น? ์ด๊ฒ์ ์ค์ํ ๋ฌธ์ ๊ฐ ์๋๊ฑฐ๋ ๋๊ตฐ๊ฐ๊ฐ ์ด์ ๋ํ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ฐพ์์ต๋๊น? ๋ํ ๋์์ด๋๋ค๋ฉด iOS 13์์ iOS 14๋ก ๊ธฐ๊ธฐ๋ฅผ ์ ๊ทธ๋ ์ด๋ํ์ ๋๋ง์ด ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค. ๊ธฐ์ตํ ์์๋ ํ iOS 13์์ ์ ๋๋ก ์๋ํ์ง๋ง ์ฐฉ๊ฐ ํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์๋ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
์ฝ๋๋ฅผ ์ ์ฅํ ๋๋ง๋ค ๋ฐ์ํ๊ณ ์ฑ์ด ํซ ๋ฆฌ๋ก๋ ๋ ๋๋ง๋ค ๋ฐ์ํฉ๋๋ค.
์ด ์๋๋ฆฌ์ค์์๋ ๋ฃจํธ ๊ตฌ์ฑ ์์๊ฐ ๋ง์ดํธ ํด์ ๋ ๋ค์ ๋ค์ ๋ง์ดํธ๋ฉ๋๋ค.
์ด๊ฒ์ ๋ด useIap()
ํํฌ์
๋๋ค.
const useIap=()=>{
const purchaseUpdateSubscription = useRef<EmitterSubscription[]>([]);
const purchaseErrorSubscription = useRef<EmitterSubscription[]>([]);
const init = useCallback(async () => {
console.taggedLog(logTag, 'call iap init');
await RNIap.initConnection();
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
purchaseUpdateSubscription.current.push(RNIap.purchaseUpdatedListener(async purchase => {
console.taggedLog(logTag, 'purchaseUpdatedListener', purchase);
try {
await finalizePurchase(purchase);
} catch (err) {
handleFailedPurchase();
}
}));
// use array to be sure to not accidentally overwrite subscriptions and lose access to them.
// being an array I can always iterate and call `remove` on them
purchaseErrorSubscription.current.push(RNIap.purchaseErrorListener(error => {
handleFailedPurchase(error);
}));
updateProducts();
return clear;
}, []);
const clear = useCallback(() => {
console.taggedLog(logTag, 'clearing all');
clearPurchaseEventListeners();
}, [clearPurchaseEventListeners]);
const clearPurchaseEventListeners = useCallback(() => {
console.taggedLog(logTag, 'clearing purchase event listener', purchaseUpdateSubscription.current.length, purchaseErrorSubscription.current.length);
purchaseUpdateSubscription.current.forEach(sub => sub.remove());
purchaseErrorSubscription.current.forEach(sub => sub.remove());
purchaseUpdateSubscription.current = [];
purchaseErrorSubscription.current = [];
}, []);
return {init,clear}
}
๊ทธ๋ฐ ๋ค์ ๋ฃจํธ ๊ตฌ์ฑ ์์์์์ด ์ฝ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
const iap=useIap();
useEffect(() => {
console.log("root component mounted");
iap.init();
return () => {
console.log("root component unmounted");
iap.clear();
};
}, []);
๋ฃจํธ ๊ตฌ์ฑ ์์๊ฐ ๋ง์ดํธ ํด์ ๋๊ณ ๋ค์ ๋ง์ดํธ ๋ ์ ์์ผ๋ฉฐ clear
๋ฉ์๋๊ฐ ํธ์ถ๋๊ณ 'clearing purchase event listener', 1,1 ๋ก๊ทธ๋ ๋ณผ ์ ์์ต๋๋ค.
์ด ์ํฉ์์๋ ๊ตฌ๋งค purchaseUpdatedListener๋ ์ฌ์ ํ ์ฌ๋ฌ ๋ฒ ํธ์ถ๋ฉ๋๋ค.
์ด๋ค ํด๊ฒฐ์ฑ ?
๊ฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
์ฌ์ง์ด ๋ค๋ฅธ ํ๋ฉด์์๋ ํธ์ถ๋์์ต๋๋ค.
์ด๊ฒ์ ๋ํ ์
๋ฐ์ดํธ๊ฐ ์์ต๋๊น?
๋์๊ฒ๋ ๊ฐ์ ๋ฌธ์ ์ ๋๋ค. ๋ค์ ๋ฒ์ iap์ด ์ด๊ธฐํ ๋ ๋ ๊ณผ๊ฑฐ ์์์ฆ์ ํธ์ถํฉ๋๋ค.
๋๋ ๋๊ฐ์ ๋ฌธ์ ๊ฐ ์์๊ณ ๊ตฌ์ฑ ์์๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์กฐ๋ฆฝํ๊ณ ๋ถํดํ์ง ์์ ์ฌ๋ฌ ์ฒญ์ทจ์๊ฐ ๋์์ ์ด๋ฆฌ๊ธฐ ๋๋ฌธ์ ๋์ผํ ์ผ์ด ๋ฐ์ํ๋ค๋ ๊ฒ์ ๊นจ๋ฌ์์ต๋๋ค.
componentWillUnmount() {
if (this.purchaseUpdateSubscription) {
this.purchaseUpdateSubscription.remove();
this.purchaseUpdateSubscription = null;
}
}
๊ตฌ๋งคํ๊ณ ๊ตฌ์ฑ ์์๋ฅผ ๋ถํด ํ ๋๋ง๋ค ๋ฆฌ์ค๋ / ๊ตฌ๋
์ ์ ๊ฑฐํ๊ณ ๊ตฌ์ฑ ์์๋ฅผ ์กฐ๋ฆฝํ ๋ ์๋ก ์ถ๊ฐํด์ผํฉ๋๋ค.
์ด ์ค๋ฅ๋ Android์์ ๋ฐ์ํ์ง ์์ต๋๋ค. ๊ทธ ์ด์ ๋ ๋ฌด์์
๋๊น? ๋ชจ๋ฅด๊ฒ ์ง๋ง Android์์ ์๋ํ๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ์ฝ๋๊ฐ ์๋ iOS์ ์๋ค๊ณ ์๊ฐํ๊ฒ ๋ง๋ญ๋๋ค.
๊ฐ๋ฅํ ํด๊ฒฐ ๋ฐฉ๋ฒ, ํ
์คํธ๋ฅผ ํตํด ๋๋๋ก ๊ณผ๊ฑฐ ์์์ฆ (์๋ฒ๋ก ์ ์ก ๋จ)์ ํ์ํ๋ฏ๋ก ์์ ํ ๊ฐ ์๋ฆผ์ transactionId๋ฅผ ์ฆ์ ์ ์ฅํ๋ฏ๋ก ํญ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์์ด transactionId๊ฐ ์ฒ๋ฆฌ๋์๋์ง ์ฌ๋ถ๋ฅผ ํ์ธํฉ๋๋ค. ๊ทธ๋ฌ๋ ๋ฌด์ (์ค๋ณต) ๊ทธ๋ ์ง ์์ผ๋ฉด ๊ณ์ํ ์ ์์ต๋๋ค. ์๋ฆผ์ด (์๋ฒ๊ฐ ์๋) ์ฑ์ ๋จผ์ ์ค๋ฉด์ด๋ฅผ ์ ์ดํ๋ โโ๋ก์ง์ ๋ฃ์ ์ ์์ต๋๋ค. ์ฆ, ํน์ ๋ฒํผ์ ํด๋ฆญ ํ ํ์ ๋ง โโ1์ ๋ฐ์์ผํฉ๋๋ค.
๊ด๋ฒ์ํ๊ฒ ํ
์คํธ ํ ํ ์ ์ ํ ๋ณต์ ๋ฅผ ์ค๋จํ๊ณ ๋ญ๊ฐ ์งํ ์ค์
๋๋ค.
์, ๋ช ๊ฐ์ง ๊ฐ๋จํ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด ์์ง๋ง ๋ชจ๋ ์์ฉ๋๋ค ....
์ด ๋ฌธ์ ๋ฅผ ์์ ํ์ญ์์ค. ์ด๊ฒ์ 2020 ๋
10 ์ ์ดํ ํ์คํ ์ค์ํ๊ณ ํ๋ก์ ํธ๋ฅผ ๊นจ๋ ๋ฒ๊ทธ์
๋๋ค ....
์ ์ผํ ํด๊ฒฐ์ฑ ์ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค : Revenuecat;)
์ด๋ค ํด๊ฒฐ์ฑ ?
Revenuecat
๊ฐ์ฅ ์ ์ฉํ ๋๊ธ
์ฌ์ ํ ๊ฐ์ ๋ฌธ์ ์ ๋๋ค. purchaseUpdatedListener๊ฐ ์์๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ต๋ํ ๋นจ๋ฆฌ ์์ ํ์ญ์์ค. ํด๊ฒฐ์ฑ ์ด ์์ต๋๋ค.