Android WeChat Pay 개발하기

Why WeChat Pay?

2015년 1월, 회사 서비스가 중국에 오픈하면서 베이징으로 출장을 다녀온 적이 있다. 처음으로 추가 했던 결제 플랫폼은 중국에서 높은 점유율을 차지하던 알리페이(Alipay)였고 1년이 지나 2016년 초에 다시 갔을 땐, 레스토랑부터 심지어 길에 있던 자판기에서 위챗페이(WeChat Pay)의 QR코드를 이용해 간편결제를 할 수 있을 만큼 성장 속도가 빨랐다. [통계로 보는 위챗] 중국 최대 메신저 ‘위챗’, ‘위챗페이’의 모든 것 다음 기사를 보면 “2016년 1분기를 기준으로 7억 6200만 명에 달합니다.”라는 내용을 볼 수 있다. 그만큼 젊은 층에서는 위챗이 알려진 만큼 위챗페이를 이용한 결제가 많아졌다.

계정 생성 및 애플리케이션 등록하기

WeChat Open Platform

개발을 시작하기 전에 중국어로 된 위챗 오픈 플랫폼에서 계정을 생성하고 앱을 등록해야 하며 이때 패키지 이름과 서명 정보를 등록해야 한다.

WeChat Payment Wiki Page

위챗페이 결제 플랫폼 위키에서 APP支付메뉴의 5번째 하위 메뉴 APP端开发步骤(APP 단말 개발)에 Android, iOS 플랫폼 별로 개발과정이 설명되어 있다. 하지만 페이지에서 다른 언어를 지원하지 않기 때문에 구글 번역기로 페이지 번역했지만 부족한 부분이 있어서 플리토(Flitto)로 번역한 한글 번역 – Android 개발 요점 설명 문서를 참고하자.

패키지 이름은 개발 중인 앱의 패키지 이름을 사용하고 서명의 경우, 위의 번역 문서대로 앱을 다운 받은 다음에 서명을 만들어서 입력하면 된다. 그리고 경험으로는 앱 정보 등록 시 서명을 만든 테스트 기기에서는 서명되지 않는 앱일지라도 위쳇 로그인이 가능하다.

SDK 설치하기

Installation Android SDK

위챗 로그인 개발로 추가된 SDK에 결제 API가 빠져서 이번에 업데이트(v3.1.1)를 진행했다. Android资源下载 페이지의 최상단에 있는 “使用微信分享、登录、收藏、支付等功能需要的库以及文件。点击下载 Android开发工具包(위쳇 모멘트, 로그인, 즐겨찾기 저장, 결제 등 기능이 필요한 문서는 Android를 통하여 다운받으시기 바랍니다)” 클릭해서 다운로드한 후 lib폴더 안에서 jar를 추가하면 된다. 만약 현재 글로벌(구글플레이)과 중국으로 서비스를 개발하고 있다면 productFlavors에서 중국 flavor를 추가해서 해당 디렉터리 안에 라이브러리 폴더를 만들어서 별도로 관리하는 편이 좋다.

안드로이드 클라이언트 개발

wechatpay_appid

개발을 시작하기 전에 위쳇 오픈 플랫폼에 앱을 등록한 후, 생성된 AppID가 필요하다. 또한 중국에 서비스를 하면서 페이팔, 알리페이, 구글 인앱결제를 개발 했었지만 위쳇페이에서는 다른 결제 플랫폼과는 다르게 통합주문(개인적으로는 선주문으로 이해함)이라는 과정이 있다. 서버 개발자라면 위쳇 페이 API 문서를 보면서 반드시 이해를 해야 하고, 클라이언트 개발자라도 결제가 정상적으로 되지 않는다면 파라미터나 해당 키값을 확인하기 위해서 알아 두는 것을 추천한다.

위쳇페이 결제 과정

wechatpay_flow

  • 1 클라이언트에서 상품의 아이디(ex: product_id)로 서비스의 서버에 주문을 넣는다.
  • 2 서버에서는 웨이신 통합 주문 API을 이용해 주문을 생성한다.
  • 3 prepaid_id가 포함된 데이터(nonce_str, mch_id, timestamp, sign)와 서비스의 상품 주문 아이디를 클라이언트에 응답으로 내려준다.
  • 4 클라이언트에서는 데이터를 가지고 위쳇 애플리케이션으로 API 요청을 보낸다.

위쳇페이 창구 열기

IWXAPI api = WXAPIFactory.createWXAPI((Context) purchasePointView, Constants.WEIXIN_APP_ID);
api.registerApp(Constants.WEIXIN_APP_ID);

if (api.isWXAppInstalled()) {
    PayReq request = new PayReq();
    request.appId = unifiedOrderItem.getAppId();
    request.nonceStr = unifiedOrderItem.getNonceStr();
    request.packageValue = "Sign=WXPay";
    request.partnerId = unifiedOrderItem.getPartnerId();
    request.prepayId = unifiedOrderItem.getPrepayId();
    request.timeStamp = String.valueOf(unifiedOrderItem.getTimeStamp());
    request.sign = unifiedOrderItem.getSign();
    api.sendReq(request);
} 
  • 5 위펫페이 결제 창구를 오픈하면서 WXPayEntryActivity가 실행된다.

WXPayEntryActivity

public class WXPayEntryActivity extends Activity implements IWXAPIEventHandler {
  public static final String TAG = "WXPayEntryActivity";
  public static final int PAYMENT_SUCCESS = 0;
  public static final int PAYMENT_ERROR = -1;
  public static final int PAYMENT_USER_CANCELED = -2;

  private IWXAPI api;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new View(this));

    api = WXAPIFactory.createWXAPI(this, Constants.WEIXIN_APP_ID);
    api.handleIntent(getIntent(), this);
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
    api.handleIntent(intent, this);
  }

  @Override
  public void onReq(BaseReq baseReq) { }

  @Override
  public void onResp(BaseResp baseResp) {
    if (baseResp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {
      Intent intent = new Intent(TAG);
      intent.putExtra("errCode", baseResp.errCode);
      sendBroadcast(intent);
    }
    finish();
  }
}
  • 6 결제를 하면 창구가 닫히면서 보낸 결제 정보를 WXPayEntryActivityonResp에서 확인한다.

WXAPIFactory

public final boolean openWXApp() {
    …
    } else {
      try {
this.context.startActivity(this.context.getPackageManager().getLaunchIntentForPackage("com.tencent.mm"));
    …
  }

WXAPIFactory에서 위쳇앱을 실행할 때, startActivity로 실행을 하기 때문에 흔히 액티비티간의 통신 방법으로 사용하는 setResult가 정상적으로 실행되지 않는다. 그래서 onResp에서 에러코드를 리턴해주기 위해 브로드캐스트(broadcase)를 사용했다.

  • 7 결제가 성공이면 서버로 부터 받은 주문 ID를 검증 요청한다.
  • 8 서버는 주문 ID를 이용해 웨이신 주문 확인 API로 주문을 최종 확인한다.
  • 9 클라이언트에게 결제 확인 응답을 보낸다.**

위쳇페이 결제 완료시 BaseResp.errorCode가 항상 -1로 리턴되는 이슈

위의 과정대로 라이브러리를 추가하고 파일을 추가해주면 간단하게 위쳇 앱에서 결제를 완료하고 개발중인 앱으로 결과까지 받아볼 수 있다. 하지만 그 결과가 -1이 되면 정상적으로 결제가 완료된 것을 뜻하지 않는다.

erroCode가 -1인 경우, WeChat Pay API 개발 문서를 보면 “발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID 설정오류, 등록된 APPID와 세팅한것 맞지 않는 경우, 그 외” 이유로 인해 발생할 수 있다. app_id의 경우 보다는 sign 값의 오류일 가능성이 높다.

클라이언트에서 sign 생성하기

// request.sign = unifiedOrderItem.getSign();

SortedMap<String, String> data = new TreeMap<>();
data.put("appid", request.appId);
data.put("noncestr", request.nonceStr);
data.put("package", request.packageValue);
data.put("partnerid", request.partnerId);
data.put("prepayid", request.prepayId);
data.put("timestamp", request.timeStamp);
request.sign = createSign(data);

private String createSign(SortedMap<String, String> parameters) {
    StringBuffer sb = new StringBuffer();
    Set es = parameters.entrySet();
    Iterator it = es.iterator();
    while (it.hasNext()) {
      Map.Entry entry = (Map.Entry) it.next();
      String k = (String) entry.getKey();
      String v = (String) entry.getValue();
      if (null != v && !"".equals(v) && !"sign".equals(k)
          && !"key".equals(k)) {
        sb.append(k + "=" + v + "&");
      }
    }
    sb.append("key=" + "00123456789vmfflxhvmfflxhvmfflxh");
    LogUtil.d("Create Sign : " + sb.toString());
    String sign = MD5Util.MD5Encode(sb.toString(), "utf-8")
        .toUpperCase();
    return sign;
  }
}

서버에서 받은 sign값을 클라이언트에서 생성해서 위쳇으로 API 요청을 할 때 보내서 테스트해보자.

// data.put("partnerid", request.partnerId);
data.put("mch_id", request.partnerId);

이때 주의할 점은 WeChat Pay 결제 확인 API 문서에 있는 대로 키 값을 mch_id으로 사용해서는 안 된다.

// data.put("prepayid", request.prepayId);
data.put("prepay_id", request.prepayId);

마찬가지로 prepay_id를 키로 사용해 만든 sign 값으로 결제를 시도하면 결제가 실패한다. 위에 있는 키 값으로 정확한 데이터를 넣어주면 문제없는 sign 값을 생성할 수 있다. 그리고 중국을 서비스하기 위해서 외부 서비스를 연동할 때, 구글 검색은 크게 도움이 되지 않는다. 이때 클래스나 코드를 기반으로 바이두 검색을 추천한다. 중국어로 이해하긴 어렵겠지만, 자신의 코드를 비교해보면 겪고 있는 문제를 조금 더 쉽게 해결할 수 있다.

[한글번역] WeChat Pay – APP端开发步骤说明

WeChat Pay App Dev Menual

원문 : APP端开发步骤说明

참고 : 아래의 번역문은 제가 직접 번역을 한 것은 아닙니다. Flitto에서 직접 한국어, 영어 번역을 참여해서 획득한 포인트로 위챗페이 개발에 필요한 문서를 번역하기 위해 번역요청(중국어->한국어)을 보내서 받은 번역문입니다. 혹시라도 위챗개발 문서가 필요한 개발자분들에게 도움이 되고자 공유합니다. 다수의 번역가가 참여했기 때문에 전체 문장이 다소 어색할 수 있음을 알려드립니다. 그리고 안드로이드 메뉴얼만 번역하였고, 4000원 비용이 들었습니다.

Android 개발 요점 설명

1. 백그라운드 설정

위챗 오픈플랫폼에서 개발 어플리케이션을 신청하시면 위챗 오픈플랫폼에서는 어플리케이션의 고유 식별 아이디를 생성해 드립니다. 안전한 결제를 위해 오픈플랫폼에 업체의 어플리케이션 패키지 이름과 어플리케이션 서명(signature)을 연동시키셔야 합니다. 연동 후에 정상적으로 결제가 가능합니다. 설정화면은 <오픈플랫폼>의 <관리센터/어플리케이션 수정/개발데이터 수정>메뉴에 있으며 그림 8.8의 빨간 네모로 표시되어 있는 부분입니다.

8.8

패키지명: 어플리케이션 프로젝트의 AndroidManifest.xml에 설정한 package 이름값이다. 예를들어 DEMO에서 package값은 net.sourceforge.simcpux.

어플리케이션 서명: 프로젝트의 패키지명 및 컴파일에 사용된 keystore을 signing툴을 사용하여 생성한 32비트길이의 MD5문자열입니다. 테스트기기에 signing툴을 설치하고 실행하면 application의 서명을 생성합니다. 그림8.9와 같이 녹색문자열이 곧 어플리케이션의 서명이다. signing툴 다운로드 url https://open.weixin.qq.com/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android.apk

8.9

2. APP ID 등록하기

WeChat jar패킷을 개발자의 앱프로젝트로 가져옵니다. 하지만, API를 사용하려면 우선 위챗으로 APPID를 등록해야 합니다. 코드는 아래와 같습니다:

final IWXAPI msgApi = WXAPIFactory.createWXAPI(context, null);
// app id를 위챗에 등록하기
msgApi.registerApp("wxd930ea5d5a258f4f");

3. 결제 호출

업체서버로부터 결제 청구를 생성합니다. 먼저 통합구매 API를 호출해(자세한 내용은 7장 참조) 구매리스트를 생성 후, prepay_id 값을 받아서 다시 사인 후 앱에 전송하여 결제 청구를 제출한다. 아래는 웨이신 결제를 호출하는 메인 코드입니다.

IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "7FFECB600D7157C5AA49810D2D8F28BC2811827B";
api.sendReq(req);

주의: sign 필드명으로 생성되는 리스트는 API 발급 설정을 참고하세요.

4. 지불결과 콜백

위쳇 SDK Sample을 참조하여, net.sourceforge.simcpux.wxapi 패캣경로에서 WXPayEntryActivity클래스를 생성합니다.(패킷명 혹은 클래스명이 일치하지 않으면 콜백이 불가능합니다), WXPayEntryActivity클래스에서 onResp 함수 구현, 지불완료 후 위쳇 엡은 판매자 어플리케이션으로 돌아가면서 onResp 함수를 콜백합니다. 개발자는 해당 함수내에서 알림메세지를 받아야하며 기존으로 돌아가기 위한 에러코드를 판단해야 합니다. 지불 성공 후 백오피스로 가서 지불결과를 체크한 후, 유저의 실제 지불결과를 보여줍니다. 클라이언트의 복귀를 유저 결제결과로 적용하지 않도록 꼭 주의해야 합니다. 서버사이드에서 받은 결제 알림메세지 혹은 API복귀결과를 체크하여 기준으로 해야 합니다, 코드예시는 다음과 같습니다:

@Override
public void onResp(BaseResp resp) {
if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX){
Log.d(TAG,"onPayFinish,errCode=" + resp.errCode);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.app_tip);
}
}

콜백한 errCode 리스트:

명칭 기술 솔루션
0 성공 성공화면
-1 오류 발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID설정오류, 등록된APPID와 세팅한것 맞지 않는 경우, 그 외
-2 사용자취소 발생가능한이유 : 사용자취소버튼 누르고 APP으로 복귀