iOS内购 - 订阅型之切换Apple ID的处理

4,585 阅读4分钟

背景

最近在做iOS内购自动续期订阅型时,发现了一些问题,特此整理一下。

问题

我开发的APP,是没有登录功能的,也就是说没有用户体系。存在如下两个问题:

  1. 我们使用第一个Apple ID(以下简称A)在一台手机上购买了自动续期型订阅。用户卸载了APP后重新安装,或者用户用A账号在另外一台手机上登录。如何延续A账号的订阅权限?
  2. 在同一台手机上,A账号购买了自动续期型订阅,随后他在手机设置里更换了Apple ID(以下简称B),如何在APP内取消订阅的权限?因为B账号并没有购买自动续期订阅,必然不能给他权限。否则可以用A账号在无数台手机上登录,然后切换账号,切换后的账号都享有自动续期订阅,这显示是一个Bug

明确几个概念

用户在安装APP后,会生成一段receipt_data的收据信息(这里笔者没有验证安装APP后,这个receipt_data收据信息是不是为空,也许不为空)。在内购支付成功后,苹果StoreKit会重新生成这么一个receipt_data的收据信息通过回调方法返回给开发者,receipt_data其实就是一段加密后的包含购买信息的收据凭证。这个receipt_data是跟Apple ID关联的,也就是说某个Apple ID购买的。

解决方案

问题1解决方案:

用户在卸载APP后重新安装,拿这个receipt_data去苹果验证接口验证,是可以拿不到当时的购买凭证的。或者用A账号在其他手机上登录的时候。很显然这个receipt_data是不包含当时A账号购买的凭证的。拿这个receipt_data去苹果验证接口验证,很显然是拿不到当时的购买凭证的(这一点需要去验证一下)。(refresh刷新receipt_data就可以通过验证)。但是用户是购买过的,所以必须在页面上提供一个”恢复购买“的按钮,让用户主动去恢复购买。恢复购买调用StoreKit的恢复购买API,支付状态的回调会被重新回调,返回新的receipt_data,此时拿这个receipt_data去苹果验证接口验证是可以看到当时的购买凭证的。所以就可以给用户订阅权限。如果用户没有点击”恢复购买“按钮,而是直接点击”购买“按钮,StoreKit会直接弹出您已经购买过的系统框,并在支付状态的回调中返回错误码,开发者可以由此判断用户是否已经购买过。

问题2解决方案:

B账号登录后,此时什么都不做,直接打印receipt_data,可以看到还是A账号登录时的receipt_data。很显然我们需要去refresh刷新receipt_data(通过调用StoreKit的刷新接口),刷新后获取新的receipt_data去苹果验证接口验证,就可以看到B账号并没有购买记录,此时提示让他去购买。但是我们并不能区分用户是否切换过账号,所以每次进入APP的时候都去刷新一下receipt_data,再去验证。那如果用户一直没有杀死APP,而是直接在手机系统里更换了Apple ID,那就无法在APP启动时刷新了,我们可以在用户具体查看订阅内容时再去刷新receipt_data并验证。

注意点

自动订阅型的记录一直存在苹果那边的(即使过期或重新购买,receipt记录还存在苹果IAP服务器中的);消耗型的finish掉苹果那边也就没有了。
客户端最好不要直接调用苹果的验证接口去验证receipt_data,可以通过自己的后端做一个中转。Java后台调用苹果验证接口部分代码如下:

private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";//沙盒测试
private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";//正式测试
private static final String IOS_SHARED_SECRET_PASSWORD = "自己APP在苹果后台设置的秘钥";//苹果连续订阅共享密钥

/**
 * 苹果服务器验证
 *
 * @param receipt 账单
 * @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
 * @url 要验证的地址
 */
public static String buyAppVerify(String receipt, int type, int status) throws Exception {
    //环境判断 线上/开发环境用不同的请求链接
    String url = "";
    if (type == 0) {
        url = url_sandbox; //沙盒测试
    } else {
        url = url_verify; //线上测试
    }
    try {
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
        URL console = new URL(url);
        HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
        conn.setSSLSocketFactory(sc.getSocketFactory());
        conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
        conn.setRequestMethod("POST");
        conn.setRequestProperty("content-type", "text/json");
        conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
        conn.setDoInput(true);
        conn.setDoOutput(true);
        BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
        String str = null;
        if (status==0){//消耗
            // 拼成固定的格式传给平台
            str = String.format(Locale.CHINA, "{"receipt-data":"" + receipt + ""}");//消耗型内购
        }else if (status == 1) {
            //连续包月订阅需要加上共享密钥
            str = String.format(Locale.CHINA, "{"receipt-data":"" + receipt + "","password":"" + IOS_SHARED_SECRET_PASSWORD + ""}");
        }

        //}
        hurlBufOus.write(str.getBytes());
        hurlBufOus.flush();

        InputStream is = conn.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        String line = null;
        StringBuffer sb = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    } catch (Exception e) {
        System.out.println("苹果服务器异常");
        e.printStackTrace();
        throw e;
    }
}