728x90

이전 포스팅

react-native-iap 가이드(1) - 설치 및 설정

https://honeystorage.tistory.com/331

 

가이드 3 - 코드작성

군말 없이 바로 코드 작성에 들어가보자

hooks 형태로 작성했으며

코드는 깔끔하지 않다

각자 입맛에 맞게 깔끔하게 정리해서 쓰도록 해보자.

 

먼저, 구현할 코드는 아래와 같다.

1. 스토어 커넥션 connection initialize

2. 리스너 붙이기 attach listener

3. 커넥션 해지 및 리스너 해지 end connection & detach listener

4. 아이템 불러오기 get products from play store / app store

5. 결제하기 payment

6. 구매 복원 restore 

 

 

1. 스토어 커넥션 connection initialize

안드로이드 개발 환경에서는 에러를 겪을 수 있다.

Not initialized ... unreachable... billing ...

별의 별 에러가 계속해서 나타나지만 무시하자.

여기서 몇 일의 시간을 허비했지만  배포 버전에는 문제 없습니다.

 

import RNIap from 'react-native-iap';
import { Platform } from 'react-native';

function useShoppingState() {
  useEffect(() => {
    const connection = async () => {
      try {
        const init = await RNIap.initConnection();
        const initCompleted = init === true;
        
        if (initCompleted) {
          if (Platform.OS === 'android') {
            await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
          } else {
            await RNIap.clearTransactionIOS();
          }
        }
        
      } catch(error) {
        console.log('connection error: ', error);
      }
    }
    
    connection();
  }, [])
}

기본 연결을 완료하였다.

 

 

2, 3. 리스너 붙이기 +  커넥션 해지 및 리스너 해지

결제 요청후 리스폰스를 받을 리스너를 등록해야하는데

가이드에서는 리스너를 connection과 함께

useEffect (혹은 componentDidmount)에 작성할것을 권장한다.

import RNIap, { InAppPurchase, PurchaseError, finishTransaction } from 'react-native-iap';
import { Platform, Alert } from 'react-native';

function useShoppingState() {
  let purchaseUpdateSubscription: any;
  let purchaseErrorSubscription: any;
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    const connection = async () => {
      try {
        const init = await RNIap.initConnection();
        const initCompleted = init === true;
        
        if (initCompleted) {
          if (Platform.OS === 'android') {
            await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
          } else {
            await RNIap.clearTransactionIOS();
          }
        }
        
        // success listener
        purchaseUpdateSubscription = purchaseUpdatedListener(
          async (purchase: InAppPurchase | SubscriptionPurchase) => {
            const receipt = purchase.transactionReceipt ? purchase.transactionReceipt : purchase.purchaseToken;
            
            if (receipt) {
              try {
                setLoading(false);
                const ackResult = await finishTransaction(purchase);
                
                // 구매이력 저장 및 상태 갱신
                if (purchase) {
                  
                }
              } catch(error) {
                console.log('ackError: ', error);
              }
            }
          }
        );
        
        purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
          setLoading(false);
         
          // 정상적인 에러상황 대응
          const USER_CANCEL = 'E_USER_CANCELED';
          if (error && error.code === USER_CANCEL) {
            Alert.alert('구매 취소', '구매를 취소하셨습니다.');
          } else {
            Alert.alert('구매 실패', '구매 중 오류가 발생하였습니다.');
          }
        });
      } catch(error) {
        console.log('connection error: ', error);
      }
    }
    
    connection();
    
    return () => {
      if (purchaseUpdateSubscription) {
        purchaseUpdateSubscription.remove();
        purchaseUpdateSubscription = null;
      }
      
      if (purchaseErrorSubscription) {
        purchaseErrorSubscription.remove();
        purchaseErrorSubscription = null;
      }
      
      RNIap.endConnection();
    }
  }, [])
}

정상적인 구매 완료가 확인되면

storage에 상태를 저장하거나

서버에 저장하는 등

각자 상황에 맞는 다음 스텝을 밟으면 된다.

 

그리고 구매 완료를 확실하게 확인하려면

RNIap의 getAvailablePurchases 메소드를 통해

확인 가능하다.

 

 

4. 아이템 불러오기

결제할 상품의 ID를 이미 알고있지만

상품을 각 스토어에서 불러오는 작업을 해주어야한다.

 

각 스토어에서 실제로 운영중인 상품에만

결제를 진행해주기위한 작업인 것으로 보인다.

 

이를 반증하듯, ID를 통해 요청했더라도

현재 운영중이지 않은 상품은 response 받을 수 없다.

response 받은 상품들의 정보를 가지고

화면에 상품 목록을 만들어 주면 된다.

 

const itemSkus: any = Platform.select({
  ios: [
    ios_item1_id,
    ios_item2_id
  ],
  android: [
    aos_item1_id,
    aos_item2_id
  ]
});

const itemSubs: any = Platform.select({
  ios: [
    ios_monthly_subs_id,
    ios_yearly_subs_id
  ],
  android: [
    aos_monthly_subs_id,
    aos_yearly_subs_id
  ]
});

function useShoppingState() {
  ...
  
  const getItems = async () => {
    try {
      const items = await RNIap.getProducts(itemSkus);
      // items 저장
      ...
    } catch(error) {
      console.log('get item error: ', error);
    }
  }
  
  const getSubscriptions = async () => {
    try {
      const subscriptions = await RNIap.getSubscriptions(itemSubs);
      // subscriptions 저장
      ...
    } catch(error) {
      console.log('get subscriptions error: ', error);
    }  
  }
}

기존의 코드에 코드를 추가했다.

itemSubs, itemSkus와 같이

각 플랫폼별 상품 ID 목록을 정의해주어야한다.

 

그렇게 받아온 상품 목록을 통해 shopping list를

사용자에게 제공하면 된다.

 

상품 정보에는

상품명, 상품 ID, 가격, 로컬라이징 가격, 상품 유형 등이 포함된다.

 

 

5. 결제하기

결제 요청도 상품 유형에 맞게 요청해야 한다.

서버로부터 받은 상품 정보에 상품 유형이 포함되어 있으니

해당 정보르르 이용해 알맞게 요청해보자.

 

function useShoppingState() {
  ...
  
  const requestItemPurchase = async (sku: string) => {
    try {
      RNIap.requestPurchase(sku);
    } catch(error) {
      console.log('request purchase error: ', error);
      Alert.alert(error.message);
    }
  }
  
  const requestSubscriptionPurchase = async (sub: string) => {
    try {
      RNIap.requestPurchase(sub);
    } catch(error) {
      console.log('request purchase error: ', error);
      Alert.alert(error.message);
    }  
  }
  
  return { requestItemPurchase, requestSubscriptionPurchase }
}

 

상품 목록을 구현한 뒤

상품 터치에 따른 구매 코드를 작성한다면

아래와 같이 상품 유형별로 나누어 작성하면 된다.

// 구매
const onPurchase = (item) => {
  if (item.type === 'subs') {
    requestSubscriptionPurchase(item.productId);
  } else {
    requestItemPurchase(item.productId);
  }
}

 

 

6. 구매 복원

애플에서 필수적으로 지원할것을

권장하는 기능인 구매복원.

 

예를들어,

1개월 구독 상품을 어제 구매했던 사용자가

이용한지 3일 정도 지난 상태에서

앱을 삭제한뒤 다시 받았을 때

구독 상품을 정상적으로 다시 이용할수 있도록

돕기 위한 기능이다.

 

위에서 언급했던

getAvaliablePurchases 메소드를 통해

구매 이력을 확인할 수 있다.

 

받아온 정보를 통해

구매했던 상품이 아직 유효한지 검증한뒤

유효 하다면 구매복원 처리 해주면 된다.

 

 

 

// Full code

import RNIap, { InAppPurchase, PurchaseError, finishTransaction } from 'react-native-iap';
import { Platform, Alert } from 'react-native';

const itemSkus: any = Platform.select({
  ios: [
    ios_item1_id,
    ios_item2_id
  ],
  android: [
    aos_item1_id,
    aos_item2_id
  ]
});

const itemSubs: any = Platform.select({
  ios: [
    ios_monthly_subs_id,
    ios_yearly_subs_id
  ],
  android: [
    aos_monthly_subs_id,
    aos_yearly_subs_id
  ]
});

function useShoppingState() {
  let purchaseUpdateSubscription: any;
  let purchaseErrorSubscription: any;
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    const connection = async () => {
      try {
        const init = await RNIap.initConnection();
        const initCompleted = init === true;
        
        if (initCompleted) {
          if (Platform.OS === 'android') {
            await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
          } else {
            await RNIap.clearTransactionIOS();
          }
        }
        
        // success listener
        purchaseUpdateSubscription = purchaseUpdatedListener(
          async (purchase: InAppPurchase | SubscriptionPurchase) => {
            const receipt = purchase.transactionReceipt ? purchase.transactionReceipt : purchase.purchaseToken;
            
            if (receipt) {
              try {
                setLoading(false);
                const ackResult = await finishTransaction(purchase);
                
                // 구매이력 저장 및 상태 갱신
                if (purchase) {
                  
                }
              } catch(error) {
                console.log('ackError: ', error);
              }
            }
          }
        );
        
        purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
          setLoading(false);
         
          // 정상적인 에러상황 대응
          const USER_CANCEL = 'E_USER_CANCELED';
          if (error && error.code === USER_CANCEL) {
            Alert.alert('구매 취소', '구매를 취소하셨습니다.');
          } else {
            Alert.alert('구매 실패', '구매 중 오류가 발생하였습니다.');
          }
        });
        
        getItems();
        getSubscriptions();
      } catch(error) {
        console.log('connection error: ', error);
      }
    }
    
    connection();
    
    return () => {
      if (purchaseUpdateSubscription) {
        purchaseUpdateSubscription.remove();
        purchaseUpdateSubscription = null;
      }
      
      if (purchaseErrorSubscription) {
        purchaseErrorSubscription.remove();
        purchaseErrorSubscription = null;
      }
      
      RNIap.endConnection();
    }
  }, [])
  
  const getItems = async () => {
    try {
      const items = await RNIap.getProducts(itemSkus);
      // items 저장
      ...
    } catch(error) {
      console.log('get item error: ', error);
    }
  }
  
  const getSubscriptions = async () => {
    try {
      const subscriptions = await RNIap.getSubscriptions(itemSubs);
      // subscriptions 저장
      ...
    } catch(error) {
      console.log('get subscriptions error: ', error);
    }  
  }
  
  const requestItemPurchase = async (sku: string) => {
    try {
      RNIap.requestPurchase(sku);
    } catch(error) {
      console.log('request purchase error: ', error);
      Alert.alert(error.message);
    }
  }
  
  const requestSubscriptionPurchase = async (sub: string) => {
    try {
      RNIap.requestPurchase(sub);
    } catch(error) {
      console.log('request purchase error: ', error);
      Alert.alert(error.message);
    }  
  }
  
  return { requestItemPurchase, requestSubscriptionPurchase }
}
728x90
반응형