728x90
* What went wrong:
Could not determine the dependencies of task ':app:mergeDebugAssets'.
> Could not resolve all task dependencies for configuration ':app:debugRuntimeClasspath'.
   > Could not find com.yqritc:android-scalablevideoview:1.0.4.
     Searched in the following locations:
       - file:/Users/user/project/lll/node_modules/react-native/android/com/yqritc/android-scalablevideoview/1.0.4/android-scalablevideoview-1.0.4.pom
       - file:/Users/user/project/lll/node_modules/jsc-android/dist/com/yqritc/android-scalablevideoview/1.0.4/android-scalablevideoview-1.0.4.pom
       - https://repo.maven.apache.org/maven2/com/yqritc/android-scalablevideoview/1.0.4/android-scalablevideoview-1.0.4.pom
       - https://dl.google.com/dl/android/maven2/com/yqritc/android-scalablevideoview/1.0.4/android-scalablevideoview-1.0.4.pom
       - https://www.jitpack.io/com/yqritc/android-scalablevideoview/1.0.4/android-scalablevideoview-1.0.4.pom

react-native-video 설치 후

빌드 과정에서 위 에러가 발생했다.

 

먼저, 설치방법

yarn add react-native-video

cd io && pod install && cd ..

npx react-native link react-native-video

 

위 에러 해결을 위한 단서는

https://github.com/react-native-video/react-native-video/issues/2454

https://stackoverflow.com/questions/68835157/error-when-trying-to-run-my-react-native-app-on-android/68841906#68841906

 

위 링크에서 확인할 수 있었다.

// android/build.gradle
allprojects {
    repositories {
        .... # Keep the rest
        jcenter() {
            content {
                includeModule("com.yqritc", "android-scalablevideoview")
            }
        }
    }
}
728x90
반응형
728x90

새 프로젝트를 시작하려 하니

내 개발환경에서 아래와 같은 이슈로

빌드에 실패하였다.

Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8." in React Native

 

원인을 찾아보니

간단한 방법으로 해결할 수 있었다.

당연하게도 java version을 맞춰주는것이다.

먼저, 맥 사용자라면 아래의 가이드를

아니라면 직접  jdk 11 버전을 설치하자.

brew tap homebrew/cask-versions
brew install --cask zulu11

 

이제, 설치한 jdk를 적용해보자

nano ~/.zshrc
# export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home

이전에 있던 1.8 버전은 주석처리하고

새 sdk를 지정한다.

그리고 저장.

 

이렇게만 하면

pc에 변경 사항이 반영되지않는다.

source ~/.zshrc

 

그리고 원래 실행중이던 터미널이 있다면

터미널을 새로 열고 빌드를 재 시도하면

정상적으로 진행될것이다.

npm run android
728x90
반응형
728x90

[업데이트]

확인 결과 .mm 기준으로

.m 레퍼런스 코드를 설정해서 작업해도

문제가 없음이 확인되었다.

아래의 내용은 꼭 필요했던 사람들만 확인 후 이용하길 바람

 

확인한 부분

- react-native-firebase/app react-native-firebase/analysis

// bash
npm i @react-native-firebase/app @react-native-firebase/analysis

// AppDelegate.mm, add
#import <Firebase.h>

[FIRApp configure];

// bash
cd ios && pod install && cd ..
npm run ios

 

 

[이전 내용]

react-native에서 새프로젝트를 만들면

AppDelegate.mm 파일이 생성되기 시작했다.

이전에는 AppDelegate.m이었다.

 

.m은 Object-C 기반

.mm은 Object-C ++ 기반

 

이라는 차이가 존재한다.

터보 모듈을 붙여 앱 성능 향상하는 것을 목적으로

RN쪽에서 업데이트한것으로 보인다.

 

다만, 개발자의 입장에서는

라이브러리들을 붙여야하는데

대부분의 현재 레퍼런스는

.m 기준으로 나타나있다.

 

따라서, .mm을 .m으로 변환해

프로젝트를 운영하고자 했다.

 

작업 순서

1. 기존의 .mm파일을 지움

2. .m 베이스 파일을 추가

3. .m 파일에 약간의 코드 변경

 

 

.m 파일에 약간의 코드 변경

#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

static void InitializeFlipper(UIApplication *application) {
  FlipperClient *client = [FlipperClient sharedClient];
  SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
  [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
  [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
  [client addPlugin:[FlipperKitReactPlugin new]];
  [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
  [client start];
}
#endif


@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  
#ifdef FB_SONARKIT_ENABLED
  InitializeFlipper(application);
#endif

  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"여기는 앱 이름"
                                            initialProperties:nil];

  if (@available(iOS 13.0, *)) {
      rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
      rootView.backgroundColor = [UIColor whiteColor];
  }

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  
  return YES;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

@end

 

1) 앱 이름 부분을 개인 설정에 맞게 넣어줄 것

2) Bridge 파트의 변경

<기존 코드>
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"  fallbackResource:nil];

<변경 코드>
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];

해당 부분의 변경을 안해주면

No visible @interface for 'RCTBundleURLProvider' declares the selector 'jsBundleURLForBundleRoot:fallbackResource:'

라는 에러를 맞이하게 된다.

728x90
반응형
728x90

이미지 압축 및 s3 업로드는

정말 많이 쓰이지만 의외로 포스팅이 없어

간단히 정리해본다.

 

 

처리절차

1. 클라이언트 요청 (생략)

2. multer-s3로 aws 업로드

3. key로 s3 object를 불러와 압축 후 재 업로드

4. 기존 파일 key로 제거

 

 

1. 클라이언트 요청 (생략)

요청은 포스트맨(Postman)으로 요청,

테스트하면 됨으로 생략 (form-data 방식 활용)

 

 

2. multer-s3로 aws 업로드

express router에서 미들웨어 처리를 해줄것이다.

그전에 multer-s3 미들웨어를 만들어주자.

// aws.ts
const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

const aws_config = {
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_KEY,
    region: 'ap-northeast-2',
    signatureVersion: 'v4',
};

export const s3 = new AWS.S3();

export const uploadImage = multer({
  storage: multerS3({
    s3,
    bucket: 'test.bucket',
    metadata: (req, file, cb) => {
      cb(null, { fieldName: file.fieldname });
    },
    key: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      const timestamp = new Date().getTime().valueOf();
      const filename = 'test' + timestamp + ext;
      cb(null, filename);
    },
  }),
});

이제 사용자 요청을 받는 router부분에서

uploadImage를 미들웨어로 넣어주면 된다.

// image.router.ts
router.post('/image/upload', uploadImage.single('photo'), (req, res) => {
  ...
})

 

 

3. key로 s3 object를 불러와 압축 후 재 업로드 & 기존 파일 key로 제거

uploadImage 처리 후 req.file에서

업로드된 이미지 객체의 key를 확인할 수 있다.

 

이 key를 활용해 객체를 불러와

압축하여 재업로드 및 삭제를 진행할것이다.

 

// aws.ts
export const compressImageUploadByKey = async (key: string, width?: number) => {
  try {
    const compressedKey = `compressed_${key}`;
    const config = {
      Bucket: 'test.bucket',
      Key: key
    }
        
    let resizedConfig: any = {
      Bucket: 'test.bucket',
      Key: compressedKey
    }

    // fetch
    const imageData: any = await s3.getObject(config).promise();

    // resizing
    const imageBuffer = await sharp(imageData.Body).resize({ width: width || 640 }).toBuffer();
    resizedConfig.Body = imageBuffer;
    
    // upload
    await s3.putObject(resizedConfig).promise();

    // origin image delete
    await s3.deleteObject(config).promise();

    return compressedKey;
  } catch(error) {
    console.log('Get image by key from aws: ', error);
  }
}

불러오고, 압축하고, 업로드하고, 지우고

4가지 작업이 진행되었다.

 

 

새로 업로드된 이미지의 Key를

DB에 저장하거나 하는 작업을 

각 작업 환경에 맞게 진행하면 끝

728x90
반응형
728x90

이전 포스팅

react-native-admob/admob 가이드 (2) - admob 설정

https://honeystorage.tistory.com/336

 

 

애드몹 관련 코드는

매우 간단하다.

 

각 플랫폼별, 광고 유형별 ID를 세팅해주고

load / show만 상황에 맞게 제어해주면 된다.

 

바로 코드를 살펴보자.

가벼운 스테이지형 게임 앱이며

특정 스테이지에 접근하려면 광고를 보여주는 상황을 가정해보자.

 

import React from 'react';
import { TouchableOpacity, Text } from 'react-native';

type Stage = {
  stage: number;
  hasAd: boolean;
}
const Stages: Stage[] = [
  {
    stage: 1,
    hasAd: false,
  },
  {
    stage: 2,
    hasAd: true,
  },
  {
    stage: 3,
    hasAd: false,
  }
];

function StageScreen({ navigation }: any) {
  const onEnterStage = (stage, hasAd) => {
    // has ad
    ...
    
    // no ad
    ...
  }
  
  return(
    <>
      { Stages.map((stage) => (
        <StageButton
          key={`curren-stage-${ stage.stage }`}
          stageNumber={ stage.stage }
          onEnter={() => {
            onEnterStage(stage.stage, stage.hasAd);
          }}
        />
      ))}
    </>
  );
}

const StageButton = ({ stageNumber, onEnter }: { stageNumber: number; onEnter: () => void; }) => {
  return (
    <TouchableOpacity onPress={onEnter}>
      <Text>Stage - { stageNumber }</Text>
    </TouchableOpacity>
  )
}

export default StageScreen;

간단히 코드를 구성해보면 이렇게 될것이다.

기본 React 코드에

광고를 보여주고, 로드하고 하는 코드만 덧 붙여주면

코드가 완성될것같다.

 

그럼 바로 추가해보자.

 

import { useInterstitialAd } from '@react-native-admob/admob';

function StageScreen({ navigation }: any) {
  const PLATFORM_FULLPAGE_AD_ID = Platform.select({
    ios: '???',
    android: '???',
  }) || '';
  
  const { adLoaded, adDismissed, show, load } = useInterstitialAd(PLATFORM_FULLPAGE_AD_ID);
  
  useEffect(() => {
    const userVisitedToAd = adLoaded && adDismissed;
    if (userVisitedToAd) {
      // stage save
      navigation.push('MyStage');
      
      // load new ad for next time
      load();
    }
    
  }, [adLoaded, adDismissed]);
  
  const onEnterStage = (stage, hasAd) => {
    if (hasAd && adLoaded && !adDismissed) {
      show();    
    } else {
      // stage save
      navigation.push('MyStage');
    }
  }
}

코드를 잠깐 살펴보자

이전의 코드에서  Stage를 클릭하면

onEnterStage가 호출됨을 알수있었다.

 

onEnterStage는

해당 Stage가 광고를 제공해야 하며(hasAd)

+ 로드되 광고가 있고(adLoaded)

+ 광고를 보여준적이 없는 상태일 때(!adDismissed)

사용자에게 광고를 보여주고

그렇지 않으면 바로 스테이지로 진입시킨다.

 

광고가 종료되면 adDismissed가 true로 바뀐다.

useEffect를 통해 해당 state의 변화를 감지해

사용자의 이전 기대 액션을 그대로 흘러가게 해준다.

(스테이지로 진입하는)

 

이때, 다음번 스테이지 진입때 광고를 보여주기위해

load를 미리 해둔다.

 

stage save는 각 환경에 맞게

글로벌 state나 storage에

현재 스테이지의 정보를 저장하라는 것이다.

 

 

개인적인 생각으로는

너무 많은 광고를 보여주면

스팸성 앱이 될수있다.

 

storage에 이전 광고 시점을 저장하여

다음 광고 시점을 20분뒤, 30분뒤

수준으로 조정하는것을 추천한다.

728x90
반응형
728x90

이전 포스팅

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

https://honeystorage.tistory.com/335

 

admob 적용을 위해서는

세가지 설정이 필요하다.

 

1. admob 설정

2. app-ads.txt 추가

3. 기타 (admob id관련 환경설정)

 

하나씩 따라가보자

어려운건 없다.

 

 

1. admob 설정

https://admob.google.com/

 

메뉴 중

앱 > 앱 추가

를 통해 플레이스토/앱스토어에 등록된(혹은 등록중인) 앱을

추가 할 수 있다.

 

아직 출시 전이라도 미리 등록해둔 뒤

나중에 연동할 수 있으니 등록을 진행하자.

 

 

2. app.ads.txt

설명을 보고

무슨 가이드에 따라서 만들고 등록 하고....

벙쪘었지만 사실은 매우 간단한

작업이었다.

궁시렁 궁시렁 글이 글지만

아래의 단계를 따라보자.

 

1. 운영중인 홈페이지가 있다.

-> https://domain/app-ads.txt 경로에 위의 코드 스니펫 복사해서 return하게 설정

 

2. 운영중인 홈페이지가 없다.

-> 구글 블로그의 app-ads.txt 기능을 이용해 위의 코드 스니펫 return하도록 설정

 

여기서 말하는 홈페이지는

앱스토어의 "앱 버전 > 지원 URL"
플레이스토어의 "스토어 설정 > 웹사이트"

에 해당한다. 도메인이 위 두 URL과 반드시 일치해야한다.

 

예를들어,

내가 등록한 도메인이

https://tistory.com 이라면

https://tistory.com/app-ads.txt에 접속했을때 

복사했던 코드 스니펫이 화면에 Text로 나타나야한다.

 

다만, 제공된 코드 스니펫만 추가하고 끝낼경우

내보낼 광고가 없어 광고가 뜨지않는 경우가 발생할 수 있다.

 

미디에이션 > 미디에이션 그룹 만들기에서

미디에이션을 만들고, 광고사와 제휴에 추가적으로

코드 스니펫을 획득할 수 있다.

 

광고사에서는

회사명/이메일/예상 광고수 등을 수집해간다.

정보를 제공하고나면 코드 스니펫을 제공해주며,

심사를 통해 제휴를 맺게된다.

제공 받은 코드 스니펫은 app-ads.txt에 추가해주면 된다.

 

 

3. ID관련 환경설정

ios, aos 모두 ID를 셋팅해주어야한다.

간단히 코드 몇줄 추가해주면된다.

아래를 보자.

// androidManifest.xml

<application
...
>
  <!-- admob -->
  <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-???~???"/>
  
  ...
</application>
// info.plist
<key>SKAdNetworkItems</key>
<array>
  <dict>
    <key>SKAdNetworkIdentifier</key>
    <string>cstr6suwn9.skadnetwork</string>
  </dict>
</array>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-???~???</string>

각 플랫폼의 App ID는

애드몹 좌측 메뉴 목록에서

"앱 (선택) > 앱 설정 > 앱 ID"에 나타나 있다.

 

 

4. 마지막, 광고 유형 추가

각각 목적에 맞게 앱 유형을 추가해주면되는데,

애드몹 좌측 메뉴 목록에서

"앱 (선택) > 광고 단위 > 광고 단위 추가"

를 통해 진행할 수 있다.

추가하고 나면 광고 단위별 광고ID (AD-ID)를 획득할 수 있는데

이를 코드상에 추가해주어야한다.

 

다음 포스팅을 통해 코드를 작성해보자.

 

 

다음 포스팅

react-native-admob/admob 가이드 (3) - 코드 작성

https://honeystorage.tistory.com/337

728x90
반응형
728x90

react-native 앱에 광고를 붙이기 위해

아래의 라이브러리를 사용하였다.

https://www.npmjs.com/package/@react-native-admob/admob

 

분명히 작업할때만 해도

deprecated상태가 아니었는데... 어느새 deprecated가 되었다

 

그렇다곤 해도

업데이트 지원을 더이상 안하는 것이지

사용 불가한 라이브러리는 아니다

 

Docs도 아직 지원한다.

https://react-native-admob.github.io/admob/docs/usage/banner

 

새로 변경된 라이브러리는

https://www.npmjs.com/package/react-native-google-mobile-ads

이것이다

 

일단,

react-native-admob/admob를 설치해보자

 

1. react-native-admob/admob 설치

npm install @react-native-admob/admob@1.5.1
or
yarn add @react-native-admob/admob@1.5.1
cd ios && pod install && cd ..

 

설치는 간단하게 끝났다.

admob에서 제일 중요한건

admob <-> 앱 연동 설정과, app-ads.txt 추가이다.

다음 포스팅에서 바로 알아보자.

 

 

다음 포스팅

react-native-admob/admob 가이드 (2) - admob 설정

https://honeystorage.tistory.com/336



728x90
반응형
728x90

이전 포스팅

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

https://honeystorage.tistory.com/331

 

 

가이드 2 - 상품 생성

코드를 작성하기에 앞서

상품을 각 스토어에 생성해주어야한다.

작업은 매우 쉽다.

 

다만, 동일 상품이라면

각 스토어에 아이디를 동일하게 생성해주자.

이렇게 하면 관리 측면에서 더 유리할 것으로 보인다.

 

 

1. IOS

 

앱 내 구입으로 들어가 보면

쉽게 상품을 추가할 수 있다.

소모품 / 비소모품 / 자동 갱신 구독 / 비자동 갱신 구독

4가지 유형의 상품이 존재한다.

 

원하는 상품을 생성하면 되며

생성할 때에는 검수 통과를 위해

상품 관련 내용을 매우 상세하게 작성해야한다.

(상품명, 가입 기간, 가입 기간동안 제공되는 혜택 및 서비스, 가격 (혹은 단위당 가격), 청구 방식, 자동 구독 방식인 경우 최소 24시간전에 취소 하지 않으면 자동 구독 결제됨을 표기, 구독 해지 방법 안내, 구매 관련 개인보호 방책 링크 등)

 

검수는 한글로 넣어도 되지만

한글을 기본으로 줄바꿈하여
번역기를 돌린 영문도 넣어주면 금상첨화

 

 

2. android

안드로이드도 크게 다르지 않다.

인앱상품 / 정기 결제에 상품을 추가해주면 된다.

ios와 달리 검수를 위한 정보를 제공할 필요도 없다.

 

 

3. 플랫폼별 ID, 앱에 정의

각 플랫폼별로 상품을 다 추가했다면

다음 포스팅의 가이드를 따라서

각 플랫폼별 상품을 skus/subs로 나누어 추가해보도락 하자.

 

다음 포스팅

react-native-iap (3) - 코드 작성

https://honeystorage.tistory.com/332

 

 

 

 

728x90
반응형
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
반응형
728x90

react-native 결제 생태계에

dooboolab 한국인 개발자 팀이 큰 기여를 했다.

 

적용에 다소 난항이 있었지만

라이브러리 소스코드 수정없이

출시된 버전을 통해

정기결제, 인앱상품 결제 구현을 완료했다.

 

dooboolab팀에 감사인사를 드리며

8.0.4 버전에 맞춰 가이드를 작성해본다.

 


가이드 1 - 설치 및 설정

yanr, npm 둘중 무엇을 사용해도 문제는 없다.

다만, 반드시 최신에 나온 안정화 버전인 8.0.4를 설치하라

(안그러면 머리카락 1000가닥쯤 빠짐)

 

yarn add react-native-iap@8.0.4
or
npm install react-native-iap@8.0.4

 

pod 설치도 마저 해주자

cd ios && pod install && cd ..

 

 xcode에서 in-app purchase도 추가해주어야 한다.

 

android에도 약간의 설정이 필요하다.

안드로이드의 설정은

google play 결제만 이용하느냐

amazon 결제도 이용하느냐에 따라 조금 바뀐다.

아래의 링크를 참고하되

google play 결제만 이용한다면 나의 가이드를 따르자

// android/build.gradle

buildscript {
  ext {
    buildToolsVersion = "30.0.2"
    minSdkVersion = 21
    compileSdkVersion = 30
    targetSdkVersion = 30
    ndkVersion = "21.4.7075529"
    
    androidXAnnotation = "1.1.0"
    androidXBrowser = "1.0.0"
  }
  
  ...
}

나는 targetSdkVersion을 29 -> 30으로 변경

androidXAnnotation, androidXBrowser 부분을

추가 해주었던 것으로 기억한다.

https://react-native-iap.dooboolab.com/docs/installing

 

// android/app/build.gradle

androd {
  ...
  
  defaultConfig {
    ...
    missingDimensionStrategy 'store', 'play'
  }
}
// androidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING"/>

설치 및 설정은 여기까지.

 

 

추가로

npm install react-native-iap을 통해

설치된 기본 버전에서는 에러가 발생했었다.

 

issue 트랙킹 결과 7.5.1로 넘어가게 됐고

거기서도 에러가 발생해

8.0.4까지 오게되었으니

 

반드시 처음부터 8.0.4를 받아서

머리카락 손실을 줄이자

 

 

 

다음 포스팅

react-native-iap (2) - 상품 생성

https://honeystorage.tistory.com/334

 

react-native-iap (3) - 코드 작성

https://honeystorage.tistory.com/332

728x90
반응형