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 값을 생성할 수 있다. 그리고 중국을 서비스하기 위해서 외부 서비스를 연동할 때, 구글 검색은 크게 도움이 되지 않는다. 이때 클래스나 코드를 기반으로 바이두 검색을 추천한다. 중국어로 이해하긴 어렵겠지만, 자신의 코드를 비교해보면 겪고 있는 문제를 조금 더 쉽게 해결할 수 있다.