背景
最近在做iOS内购自动续期订阅型时,发现了一些问题,特此整理一下。
问题
我开发的APP,是没有登录功能的,也就是说没有用户体系。存在如下两个问题:
- 我们使用第一个
Apple ID(以下简称A)在一台手机上购买了自动续期型订阅。用户卸载了APP后重新安装,或者用户用A账号在另外一台手机上登录。如何延续A账号的订阅权限? - 在同一台手机上,
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;
}
}