React-native-iap: ๊ตฌ๋งค ํ›„์—๋„ ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœ๋˜๋Š” purchaseUpdatedListener

์— ๋งŒ๋“  2020๋…„ 10์›” 26์ผ  ยท  22์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: dooboolab/react-native-iap

react-native-iap ๋ฒ„์ „

5.0.1

๋ฐ˜์‘ ๋„ค์ดํ‹ฐ๋ธŒ ๋ฒ„์ „

0.63.3

์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ํ”Œ๋žซํผ (IOS ๋˜๋Š” Android ๋˜๋Š” ๋‘˜ ๋‹ค?)

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;
    }
  };
}, []);

๐Ÿ“ฑ iOS ๐Ÿ™ help wanted

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

์—ฌ์ „ํžˆ ๊ฐ™์€ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. purchaseUpdatedListener๊ฐ€ ์†์ƒ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ตœ๋Œ€ํ•œ ๋นจ๋ฆฌ ์ˆ˜์ •ํ•˜์‹ญ์‹œ์˜ค. ํ•ด๊ฒฐ์ฑ…์ด ์—†์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  22 ๋Œ“๊ธ€

์ด๋ฒคํŠธ๊ฐ€ ๋™์ผํ•œ ๊ตฌ๋งค ๊ฒฐ๊ณผ๋ฅผ ๋ฐœ์ƒํ•ฉ๋‹ˆ๊นŒ?

์˜ˆ, ์„ฑ๊ณต์ ์ธ ๊ฒฐ๊ณผ / ์˜์ˆ˜์ฆ๊ณผ ํ•จ๊ป˜ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์˜์ˆ˜์ฆ์ด ์—†์œผ๋ฉด ์‹คํ–‰๋˜์ง€๋งŒ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์„œ๋ฒ„์—์„œ๋Š” ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋‹ค์‹œ ๋ Œ๋”๋ง๋˜๊ณ  ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋ช‡ ๋ฒˆ์— ๊ฑธ์ณ ์‹œ์ž‘๋  ๋•Œ ๋งŽ์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฑด ํž˜๋“ค๊ฒŒ ๊ณต๊ฐœ ๋˜๊ฒ ์ง€๋งŒ ์‚ฌ๊ฑด์„ ์žฌํ˜„ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

์Œ, "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์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋ ค๊ณ ํ–ˆ์ง€๋งŒ ๋™์ผํ•œ ๋™์ž‘์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋™์ž‘์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์‚ฌ์šฉ์ž๊ฐ€ IAP๋ฅผ ๊ตฌ๋งคํ•ฉ๋‹ˆ๋‹ค.
  2. PurchaseUpdatedListener๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  3. ์‚ฌ์šฉ์ž๊ฐ€ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
  4. ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.
  5. 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

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰