0 code iap ios

2016-04-29 13:41:03 u010246789 阅读数 16876

场景:作为后台需要为app提供服务,在ios中,app内进行支付购买时需要进行二次验证。

基础:可以参考上一篇转载的博文In-App Purchase(iap)快速指南了解原理。

直接先上服务端测试通过的代码:

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.alibaba.fastjson.JSONObject;

@Controller
@RequestMapping("iap")
public class IapController {
	
	//购买凭证验证地址
	private static final String certificateUrl = "https://buy.itunes.apple.com/verifyReceipt";
	
	//测试的购买凭证验证地址 
	private static final String certificateUrlTest = "https://sandbox.itunes.apple.com/verifyReceipt";
	
	/**
	 * 重写X509TrustManager
	 */
	private static TrustManager myX509TrustManager = new X509TrustManager() {
		
		@Override
		public X509Certificate[] getAcceptedIssuers() {
			return null;
		}
		
		@Override
		public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
			
		}
		
		@Override
		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
			
		}
	};
	
	/**
	 * 接收iOS端发过来的购买凭证
	 * @param userId 
	 * @param receipt
	 * @param chooseEnv
	 */
	@RequestMapping("/setIapCertificate")
	public String setIapCertificate(String userId, String receipt, boolean chooseEnv){
		if(StringUtils.isEmpty(userId) || StringUtils.isEmpty(receipt)){
			return null;
		}
		String url = null;
		url = chooseEnv == true? certificateUrl:certificateUrlTest;
		final String certificateCode = receipt;
		if(StringUtils.isNotEmpty(certificateCode)){
			return sendHttpsCoon(url, certificateCode);
		}else{
			return null;
		}
	}
	
	/**
	 * 发送请求
	 * @param url
	 * @param strings
	 * @return
	 */
	private String sendHttpsCoon(String url, String code){
		if(url.isEmpty()){
			return null;
		}
		try {
			//设置SSLContext
			SSLContext ssl = SSLContext.getInstance("SSL");
			ssl.init(null, new TrustManager[]{myX509TrustManager}, null);
			
			//打开连接
			HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
			//设置套接工厂
			conn.setSSLSocketFactory(ssl.getSocketFactory());
			//加入数据
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);
			conn.setRequestProperty("Content-type","application/json");
	        
	        JSONObject obj = new JSONObject();
	        obj.put("receipt-data", code);
	        
	        BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
	        buffOutStr.write(obj.toString().getBytes());
	        buffOutStr.flush();
	        buffOutStr.close();
	        
	        //获取输入流
	        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
	        
	        String line = null;
	        StringBuffer sb = new StringBuffer();
	        while((line = reader.readLine())!= null){
	        	sb.append(line);
	        }
	        return sb.toString();
		
		} catch (Exception e) {
			return null;
		}
	}
}

注意:

  1. setIapCertificate()方法的输入输出参数自行与ios商定,这里是直接将apple返回的数据返给了app(个人推荐返回boolean,前台只要知道验证是否通过即可)。
  2. 参数receipt:BASE64编码过的话
    ewoJInNpZ25hdHVyZSIgPSAiQXAzbjVUZmQrVXIvQmFrTEU3VG9Dc3NOSGdxbmI2dnlrM0RLZlFxcnBxQStLV1AzVHVkZDN2N2w0OUEyc2NVY1g2cWNDeEx3bHAzR2RKMG9Ma3IzRzdTaEZFYU5UeTBrL25hRTNoWUIxY0kxajFGM1ppSmxTVC9kL1VDN0ZFY1BUMjR2SUFFZDE2WXFOdHNqWUpNYmJQVGRRR3g4NitFZWlDR21mVDJKS0VrK0FBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RXpNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdUxnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25iMzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQTFVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hRd3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tkluclp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSjNwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQU0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUyTFRBMExUSTRJREF6T2pFNE9qUTVJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeUlpQTlJQ0prTkdVM01qRmxZelkzWldZeVptVmpZVGRtWW1SaVpESTFZVFExWTJaaU16ZGxNVEJsWVRkaUlqc0tDU0p2Y21sbmFXNWhiQzEwY21GdWMyRmpkR2x2YmkxcFpDSWdQU0FpTVRBd01EQXdNREl3T0RZeU1EUTNNQ0k3Q2draVluWnljeUlnUFNBaU1TNHhJanNLQ1NKMGNtRnVjMkZqZEdsdmJpMXBaQ0lnUFNBaU1UQXdNREF3TURJd09EWXlNRFEzTUNJN0Nna2ljWFZoYm5ScGRIa2lJRDBnSWpFaU93b0pJbTl5YVdkcGJtRnNMWEIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFME5qRTRNemczTWpreU9EVWlPd29KSW5WdWFYRjFaUzEyWlc1a2IzSXRhV1JsYm5ScFptbGxjaUlnUFNBaU9FVXhPVVZGUXpRdE16TkVOeTAwTlRNMkxVSTJNa1V0TVRFeVFrRkROamhGUlVORUlqc0tDU0p3Y205a2RXTjBMV2xrSWlBOUlDSXhNalEwSWpzS0NTSnBkR1Z0TFdsa0lpQTlJQ0l4TVRBNE56azRNVFV4SWpzS0NTSmlhV1FpSUQwZ0ltTnZiUzVrYjJOMGIzSkllWE1pT3dvSkluQjFjbU5vWVhObExXUmhkR1V0YlhNaUlEMGdJakUwTmpFNE16ZzNNamt5T0RVaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVaUlEMGdJakl3TVRZdE1EUXRNamdnTVRBNk1UZzZORGtnUlhSakwwZE5WQ0k3Q2draWNIVnlZMmhoYzJVdFpHRjBaUzF3YzNRaUlEMGdJakl3TVRZdE1EUXRNamdnTURNNk1UZzZORGtnUVcxbGNtbGpZUzlNYjNOZlFXNW5aV3hsY3lJN0Nna2liM0pwWjJsdVlXd3RjSFZ5WTJoaGMyVXRaR0YwWlNJZ1BTQWlNakF4Tmkwd05DMHlPQ0F4TURveE9EbzBPU0JGZEdNdlIwMVVJanNLZlE9PSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbmRib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0=
    未进行编码的话:
    {
    	"signature" = "Ap3n5Tfd+Ur/BakLE7ToCssNHgqnb6vyk3DKfQqrpqA+KWP3Tudd3v7l49A2scUcX6qcCxLwlp3GdJ0oLkr3G7ShFEaNTy0k/naE3hYB1cI1j1F3ZiJlST/d/UC7FEcPT24vIAEd16YqNtsjYJMbbPTdQGx86+EeiCGmfT2JKEk+AAADVzCCA1MwggI7oAMCAQICCBup4+PAhm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXDTE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZhS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJV2U51rxfcqAAe5C2/fEW8KUl4iO4lMuta7N6XzP1pZIz1NkkCtIIweyNj5URYHK+HjRKSU9RLguNl0nkfxqObiMckwRudKSq69NInrZyCD66R4K77nb9lMTABSSYlsKt8oNtlhgR/1kjSSRQcHktsDcSiQGKMdkSlp4AyXf7vnHPBe4yCwYV2PpSN04kboiJ3pBlxsGwV/ZlL26M2ueYHKYCuXhdqFwxVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGTOvQ==";
    	"purchase-info" = "ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE2LTA0LTI4IDAzOjE4OjQ5IEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICJkNGU3MjFlYzY3ZWYyZmVjYTdmYmRiZDI1YTQ1Y2ZiMzdlMTBlYTdiIjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDIwODYyMDQ3MCI7CgkiYnZycyIgPSAiMS4xIjsKCSJ0cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDIwODYyMDQ3MCI7CgkicXVhbnRpdHkiID0gIjEiOwoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtbXMiID0gIjE0NjE4Mzg3MjkyODUiOwoJInVuaXF1ZS12ZW5kb3ItaWRlbnRpZmllciIgPSAiOEUxOUVFQzQtMzNENy00NTM2LUI2MkUtMTEyQkFDNjhFRUNEIjsKCSJwcm9kdWN0LWlkIiA9ICIxMjQ0IjsKCSJpdGVtLWlkIiA9ICIxMTA4Nzk4MTUxIjsKCSJiaWQiID0gImNvbS5kb2N0b3JIeXMiOwoJInB1cmNoYXNlLWRhdGUtbXMiID0gIjE0NjE4Mzg3MjkyODUiOwoJInB1cmNoYXNlLWRhdGUiID0gIjIwMTYtMDQtMjggMTA6MTg6NDkgRXRjL0dNVCI7CgkicHVyY2hhc2UtZGF0ZS1wc3QiID0gIjIwMTYtMDQtMjggMDM6MTg6NDkgQW1lcmljYS9Mb3NfQW5nZWxlcyI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZSIgPSAiMjAxNi0wNC0yOCAxMDoxODo0OSBFdGMvR01UIjsKfQ==";
    	"environment" = "Sandbox";
    	"pod" = "100";
    	"signing-status" = "0";
    }
  3. 向苹果发起验证请求的参数一定要通过json格式发送:
    conn.setRequestProperty("Content-type","application/json");
    JSONObject obj = new JSONObject();
    obj.put("receipt-data", code);
  4. App Store返回值也是一个JSON对象:
    {
    “status” : 0,
    “receipt” : { … }
    }
    具体我这边是这样的:
    <pre name="code" class="javascript">{
    	"receipt": {
    		"original_purchase_date_pst": "2016-04-28 03:18:49 America/Los_Angeles",
    		"purchase_date_ms": "1461838729285",
    		"unique_identifier": "d4e721ec67ef2feca7fbdbd25a45cfb37e10ea7b",
    		"original_transaction_id": "1000000208620470",
    		"bvrs": "1.1",
    		"transaction_id": "1000000208620470",
    		"quantity": "1",
    		"unique_vendor_identifier": "8E19EEC4-33D7-4536-B62E-112BAC68EECD",
    		"item_id": "1108798151",
    		"product_id": "1244",
    		"purchase_date": "2016-04-28 10:18:49 Etc/GMT",
    		"original_purchase_date": "2016-04-28 10:18:49 Etc/GMT",
    		"purchase_date_pst": "2016-04-28 03:18:49 America/Los_Angeles",
    		"bid": "com.doctorHys",
    		"original_purchase_date_ms": "1461838729285"
    	},
    	"status": 0
    }


    
    


2018-02-07 18:41:10 qq_30211165 阅读数 2414

内购全面总结
苹果IAP最大的坑点:applicationUsername=nil,你懂得
另外:IAP和第三方支付最大的不同点
第三方支付:客户端只要给服务器传商品参数给服务器让我们服务器向第三方支付服务器请求交易订单这样的好处是安全,可控制,可查询然后我们客户端根据服务器给我们的交易订单来拉起支付

但是IAP:如果也向第三方流程一样由服务器创建订单再下发给客户端然后调用IAP的话我们无法控制这笔订单是否成功不可控制因素太多,比如这笔交易没有成功而服务器已经生产了订单然后客户端根据苹果的交易结果去服务器验证在有服务器去向IAP验证延时太长。所以内购我们不能按照这样的流程来
而是客户端先向IAP发起请求支付,交易完成以后客户端再去我们自己的服务器创建订单然后再让服务器根据证订单再向苹果服务器去验证这笔交易是否完成最后通知客户端
第三方支付的流程图(此图来源简书:NewPan)

内购流程
这里写图片描述

一:协议
1.协议,税务和银行业务信息的填写
2.内购商品的添加
3.添加沙盒测试账号
4.内购代码具体实现
5.内购注意事项
二.协议.税务和银行信息的填写入口
• 2.2、选择申请合同类型
• 进入协议、税务和银行业务页面后,会有3种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有iOS Free Application一种。
• 页面内容分为两块:
Request Contracts(申请合同)
Contracts In Effect(已生效合同)。
• 合同类型分为3种:
iOS Free Application(免费应用合同)
iOS Paid Application(付费应用合同)
iAd App NetNetwork(广告合同)

2.3、申请iOS Paid Application合同(协议、税务和银行业务3个都要填写)

先点击Contact Info 的Set Up

有些银行通过下面的Look up CNAPS Code方法查不到,就需要借助百度了,一定要准确查询,否则会有问题。推荐一个地址
https://e.czbank.com/CORPORBANK/query_unionBank_index.jsp如果查不到自己的银行cnaps code可以打电话给银行客服

货币类型可能有歧义,看你是想收美元还是人民币了,都说美元合适。不过,我做的时候为了避免事情,还是选择了CNY,支持国产。还有一点,银行账号如果是对公的账号,需要填写公司的英文名称,如果没有的话,上拼音!然后点击保存银行信息就算ok了,然后退回到最开始的页面

如果以上信息填写完毕,状态一直是Processing不要怀疑自己填写出错,那是需要审核一般1到3天就能通过

二、为app添加内购产品

在你点击添加内购产品按钮后会有弹框,提示你选择类型,这个就要看你app的需求了

填写完审核信息后,点击右上角的“存储”按钮,就添加了一个内购产品~

三、添加沙盒技术测试员
在iTunes Connect的用户和智能中选择“沙盒技术测试员”,填写信息保存以后就有一个测试员了

四、具体实现

import

import

pragma mark–内购代理

//收到产品返回信息这个时获取苹果服务器的产品列表根据id来的
- (void)productsRequest:(SKProductsRequest )request didReceiveResponse:(SKProductsResponse )response{

// NSLog(@”————–收到产品反馈消息———————”);
NSArray *product = response.products;
if([product count] == 0){
// [SVProgressHUD dismiss];
// NSLog(@”————–没有商品——————”);
return;
}

// NSLog(@”productID:%@”, response.invalidProductIdentifiers);
// NSLog(@”产品付费数量:%lu”,(unsigned long)[product count]);

SKProduct *p = nil;
for (SKProduct *pro in product) {
    NSLog(@"%@", [pro description]);
    NSLog(@"%@", [pro localizedTitle]);
    NSLog(@"%@", [pro localizedDescription]);
    NSLog(@"%@", [pro price]);
    NSLog(@"%@", [pro productIdentifier]);



    if([pro.productIdentifier isEqualToString:@"jianyinyue6"]){
        p = pro;
    }
}


SKPayment *payment = [SKPayment paymentWithProduct:p];

// NSLog(@”发送购买请求”);
[[SKPaymentQueue defaultQueue] addPayment:payment];
// _hud.hidden = YES;

}

//请求失败
- (void)request:(SKRequest )request didFailWithError:(NSError )error{
// [SVProgressHUD showErrorWithStatus:@”支付失败”];
_hud.hidden = YES;
// NSLog(@”——————错误—————–:%@”, error);
}

  • (void)requestDidFinish:(SKRequest *)request{
    // [SVProgressHUD dismiss];
    // NSLog(@”————反馈信息结束—————–”);
    }
    //沙盒测试环境验证

define SANDBOX @”https://sandbox.itunes.apple.com/verifyReceipt

//正式环境验证

define AppStore @”https://buy.itunes.apple.com/verifyReceipt

//
-(void)verifyPurchaseWithPaymentTransaction{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];

NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串

NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];


//创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:AppStore];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
   NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);


    return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];

// NSLog(@”%@”,dic);
if([dic[@”status”] intValue]==0){
// NSLog(@”购买成功!”);
//验证过

    NSDictionary *dicReceipt= dic[@"receipt"];
    NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
    NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
    //如果是消耗品则记录购买数量,非消耗品则记录是否购买过
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
    if ([productIdentifier isEqualToString:@" "]) {

// 以后确认购买成功了
[self cfingMBhud];
[[NSUserDefaults standardUserDefaults] setValue:productIdentifier forKey:@”appidsting”];
[[NSUserDefaults standardUserDefaults]synchronize];
}else{
[defaults setBool:YES forKey:productIdentifier];
}
//在此处对购买记录进行存储,可以存储到开发商的服务器端
}else{

    //从沙盒中获取交易凭证并且拼接成请求体数据
    NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];

    NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串

    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];


    //创建请求到苹果官方进行购买验证
    NSURL *url=[NSURL URLWithString:SANDBOX];
    NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
    requestM.HTTPBody=bodyData;
    requestM.HTTPMethod=@"POST";
    //创建连接并发送同步请求
    NSError *error=nil;
    NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
    if (error) {
        NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);


        return;
    }
    NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
    NSLog(@"%@",dic);
    if([dic[@"status"] intValue]==0){
        //        NSLog(@"购买成功!");
        //验证过
        _hud.hidden = YES;
        NSDictionary *dicReceipt= dic[@"receipt"];
        NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
        NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
        //如果是消耗品则记录购买数量,非消耗品则记录是否购买过
        NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
        if ([productIdentifier isEqualToString:@"jianyinyue6"]) {

            //            以后确认购买成功了
            [self cfingMBhud];
            [[NSUserDefaults standardUserDefaults] setValue:productIdentifier forKey:@"appidsting"];
            [[NSUserDefaults standardUserDefaults]synchronize];
        }else{
            [defaults setBool:YES forKey:productIdentifier];
        }
    }else{
            _hud.hidden = YES;

    }

       }

}

//监听购买结果
- (void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
// NSLog(@”交易完成”);
_hud.hidden = YES;
// 发送到苹果服务器验证凭证
[self verifyPurchaseWithPaymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
// NSLog(@”商品添加进列表”);
break;
case SKPaymentTransactionStateRestored:{
// NSLog(@”已经购买过商品”);
_hud.hidden= YES;
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
// NSLog(@”交易失败%@”,tran);
// [self verifyPurchaseWithPaymentTransaction];
_hud.hidden = YES;
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
default:
break;
}
}
}
//可以知道恢复购买购买了哪些东西这里可以喝服务器做交互
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{

NSMutableArray *ar = [[NSMutableArray alloc] init];
NSLog(@"received restored transactions: %lu", (unsigned long)queue.transactions.count);
//没有购买过
if (queue.transactions.count==0) {
    _hud.hidden = YES;
    _hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    _hud.mode =  MBProgressHUDModeText;
    _hud.removeFromSuperViewOnHide=NO;
    _hud.label.text = @"哦哦,你还没购买过此项目,赶快去购买吧!";
    _hud.bezelView.color = [UIColor blackColor];
    [_hud hideAnimated:YES afterDelay:2.0];
}
//购买过
for (SKPaymentTransaction *transaction in queue.transactions)
{
    NSString *productID = transaction.payment.productIdentifier;
    [ar addObject:productID];


    [[NSUserDefaults standardUserDefaults] setValue:ar[0] forKey:@"appidsting"];
     [[NSUserDefaults standardUserDefaults]synchronize];

    [self cfingMBhud];
}

}

//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
// NSLog(@”交易结束”);
_hud.hidden = YES;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

  • (void)dealloc{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }

购买成功后我们iOS前端可以单独在客户端完成订单正确性的验证。但是因为有的项目后台要Android和iOS两端生成账单便于对账。所以我们请求后台接口,服务器处验证是否支付成功,依据后台返回结果做相应逻辑处理。

订单正确性的验证本来可以是:iOS客户端(购买成功)→ 前端到苹果服务器验证→处理苹果返回结果做相应逻辑处理; 现在:iOS客户端(购买成功)→ 后台→后台到苹果服务器验证→处理后台返回结果做相应逻辑处理)

服务器要做的是:
1.接收iOS前端发过来的购买凭证。
2.判断凭证是否已经存在或验证过,然后存储该凭证。
3.将该凭证发送到对应环境下的苹果服务器验证,并将验证结果返回给客户端。
4.根据需求,是否修改用户相应信息。
官方文档应该也是支持的这么做的→In-App Purchase Programming Guide
- (void)verifyTransactionResult{

//验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];

//从沙盒中获取到购买凭据
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
//传输的是BASE64编码的字符串
BASE64常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性,BASE64是可以编码和解码的。

NSDictionary *requestDict =@{@”receipt-data”: [receipt base64EncodedStringWithOptions:0],@”sandbox”:@”1”};

请求后台接口,服务器处验证是否支付成功,依据返回结果做相应逻辑处理
与后台协调好,让后台根据你的“sandbox”字段的1,0来区分请求是正式环境还是测试环境
当然“sandbox”这个字段也可以替换为你想要的,但是“receipt-data”不能替换,要注意!)
//请求成功的response自己输出看一下吧,status是0就成功了,这里就不贴出来了,因为有一些敏感数据,比如你的bundleID,product_id之类的
}

下面是两种环境下的苹果服务器验证地址
测试环境(审核用这个)

define SANDBOX @”https://sandbox.itunes.apple.com/verifyReceipt

//正式环境验证

define AppStore @”https://buy.itunes.apple.com/verifyReceipt

五、要注意的事项!
1.bundleID要与iTunes Connect上你App的相同,不然是请求不到产品信息的
2.在沙盒环境进行测试内购的时候,要使用没有越狱的苹果手机。
3.在沙盒环境下真机测试内购时,请去app store中注销你的apple ID,不然发起支付购买请求后会直接case:SKPaymentTransactionStateFailed。使用沙盒测试员的账号时不需要真正花钱的。
4.如果只添加了一个沙盒测试员账号,当一个真机已经使用了这个账号,另一个真机再使用这个账号支付也是会发生错误的。那就去多建几个沙盒测试员账号使用不同的,反正也是免费的,填写也很快。
5.监听购买结果,当失败和成功时代码中要调用:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
该方法通知苹果支付队列该交易已完成,不然就会已发起相同 ID 的商品购买就会有此项目将免费恢复的提示

六、请在本地做一下凭证存储!
现在订单正确性的验证是:iOS客户端(购买成功)→ 后台→后台到苹果服务器验证→处理后台返回结果做相应逻辑处理。
我们前端购买成功后,凭证本地保留一份,当与后台验证成功后,再将本地保留的凭证删除。否者一直使用本地已经保留的凭证与后台交互。
注:由于以前我在的iTunes Connect中填写过协议,税务,和银行业务步骤无法复现所以部分图片来自简书作者:睡不着的叶-《iOS开发 内购流程 手把手教你还不学?》文章。

七:出现的问题
如果在真机上运行代码请将真是apple id账号退出用测试账号登入
App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。
审核不通过:原因没有提供测试账号,或者审核时用的是正式环境验证链接
如果年会员或者月会员的话选择是—消耗性商品,订阅和消耗性返回数据有区别
生成的订单怎么对应上这个订单呢-》后台来做(推荐)也可以前台来做
凭证存储一定要在本地做一下(防止网络原因或者和后台中断访问)然后等和后台交互过确认以后再删除

八:坑点(转自简书NewPan)
最大的一个就是,从 IAP 交易结果出来到通知 APP,只有一次。
1.如果用户后买成功以后,网络就不行了,那么苹果的 IAP 也收不到支付成功的通知,就没法通知 APP,我们也没法给用户发货。
2.如果 IAP 通知我们支付成功,我们驱动服务器去 IAP 服务器查询失败的话,那就要等下次 APP 启动的时候,才会重新通知我们有未验证的订单。这个周期根本没法想象,如果用户一个月不重启 APP,那么我们可能一个月没法给用户发货
3.有人反馈,IAP 通知已经交易成功了,此时去沙盒里取收据数据,发现为空,或者出现通知交易成功那笔交易没有被及时的写入到沙盒数据中,导致我们服务器去 IAP 服务器查询的时候,查不到这笔订单。
4.如果用户的交易还没有得到验证,就把 APP 给卸载了,以后要怎么恢复那些没有被验证的订单?
5.越狱手机有无数奇葩的收据丢失或无效或被替换的问题,应该怎样酌情处理?
解决:越狱手机一律不准内购
检查是否越狱:友盟统计有一个方法#import

2017-09-05 15:47:29 u011452278 阅读数 2079

iOS应用内购买项目流程,大致分为三个部分:

第一部分:iTunes connect设置,包含创建新的发布版本号、创建购买项目(商品)、设置协议税务银行业务、添加沙盒技术测试账号;


第二部分:在项目Xcode中设置允许In-App Purchase,然后编写代码;代码部分可以使用原生态的,也可以使用第三方集成好的(IAPHelper),需要注意本地存储交易凭证,用于网络不佳或失败的情况下,重新提交。代码完成好之后,打包提交到APP Strore等待审核,提交的同时也把之前的内购项目勾选一并提交审核,待审核通过就可以测试了(可能审核通过之后需等待24小时,才能购买成功);


第三部分:测试是否成功购买,这里需要真机测试,并注销本机的Apple ID,然后在app中点击购买的时候,输入之前添加的沙盒测试账号。这部分可能会报各种错误,可能是前面一二部分没有设置完善,也有可能APP Store的问题,需要等待24小时自动就可以了,总之有问题就会有对应的解决方法,网上博客的解决方法也会帮助你的。


前面的设置部分按照开发文档设置,这里直接上代码:

//去苹果服务器请求商品
- (void)requestProductData:(NSString *)productId{
    NSSet* dataSet = [[NSSet alloc] initWithArray:@[productId]];
    [IAPShare sharedHelper].iap = [[IAPHelper alloc] initWithProductIdentifiers:dataSet];
#if DEBUG==1
    [IAPShare sharedHelper].iap.production = NO;
#else
    [IAPShare sharedHelper].iap.production = YES;
#endif
    // 请求商品信息
    [[IAPShare sharedHelper].iap requestProductsWithCompletion:^(SKProductsRequest* request,SKProductsResponse* response){
         if(response.products.count > 0 ) {
             SKProduct *product = response.products[0];
             [[IAPShare sharedHelper].iap buyProduct:product onCompletion:^(SKPaymentTransaction* trans){
                 if(trans.error){
                     [self.hud hideAnimated:YES];
                     [MBProgressHUD showError:@"购买失败,请重试" toView:self.view];
                }else if(trans.transactionState == SKPaymentTransactionStatePurchased) {
                    self.receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
                    [self justifyIAP:self.hud];
                }else if(trans.transactionState == SKPaymentTransactionStateFailed) {
                    if (trans.error.code == SKErrorPaymentCancelled) {
                        [self.hud hideAnimated:YES];
                    }else{
                        [[[CommonUIAlert alloc] init] showCommonAlertView:self title:@"" message:@"购买失败,请重试" cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancle:^{
                            [self.hud hideAnimated:YES];
                            [self saveReceipt];
                        } confirm:^{
                            [self justifyIAP:self.hud];
                        }];
                    }
                }
            }];
         }else{
             [self.hud hideAnimated:YES];
             [MBProgressHUD showError:@"购买失败,请重试" toView:self.view];
         }
     }];
}

#pragma mark - ios iap应用内支付二次验证
- (void)justifyIAP:(MBProgressHUD*)hud{
    if(hud==nil){
        hud = [MBProgressHUD showMessag:@"支付中..." toView:self.view];
    }
    NSString *receiptBase64 = [NSString base64StringFromData:self.receipt length:[self.receipt length]];
    __weak typeof(self) weakSelf = self;
    NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
    [requestDict setObject:[DataModelInstance shareInstance].userModel.userId forKey:@"userid"];
    [requestDict setObject:[CommonMethod paramStringIsNull:receiptBase64] forKey:@"data"];
    [self requstType:RequestType_Post apiName:API_NAME_USER_POST_USER_IAPSECONDVALID paramDict:requestDict hud:hud success:^(AFHTTPRequestOperation *operation, id responseObject, MBProgressHUD *hud) {
        if([CommonMethod isHttpResponseSuccess:responseObject] == HttpResponseTypeSuccess){
            weakSelf.receipt = nil;
            [hud hideAnimated:YES];
            [weakSelf.navigationController popViewControllerAnimated:YES];
        }else{
            [[[CommonUIAlert alloc] init] showCommonAlertView:self title:@"" message:@"购买失败,请重试" cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancle:^{
                [hud hideAnimated:YES];
                [weakSelf saveReceipt];
            } confirm:^{
                [weakSelf justifyIAP:hud];
            }];
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error, MBProgressHUD *hud) {
        [[[CommonUIAlert alloc] init] showCommonAlertView:self title:@"" message:@"购买失败,请重试" cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancle:^{
            [hud hideAnimated:YES];
            [weakSelf saveReceipt];
        } confirm:^{
            [weakSelf justifyIAP:hud];
        }];
    }];
}

//持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不同的凭证)
- (void)saveReceipt{
    if(self.receipt && self.receipt.length){
        NSString *fileName = [NSString genUUID];
        NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName];
        NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys: self.receipt, IAP_RECEIPT,[DataModelInstance shareInstance].userModel.userId,IAP_USER_ID,nil];
        BOOL result = [dic writeToFile:savedPath atomically:YES];
        NSLog(@"%d",result);
        self.receipt = nil;
    }
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        
        
    //从服务器验证receipt失败之后,在程序再次启动的时候,使用保存的receipt再次到服务器验证
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//如果在改路下不存在文件,说明就没有保存验证失败后的购买凭证,也就是说发送凭证成功。
        [fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//创建目录
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:nil];
    }else{//存在购买凭证,说明发送凭证失败,再次发起验证
        [self sendFailedIapFiles];
    }
    return YES;
}

//验证receipt失败,App启动后再次验证
- (void)sendFailedIapFiles{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    
    //搜索该目录下的所有文件和目录
    NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error];
    
    if (error == nil){
        for (NSString *name in cacheFileNameArray){
            if ([name hasSuffix:@".plist"]){//如果有plist后缀的文件,说明就是存储的购买凭证
                NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name];
                [self sendAppStoreRequestBuyPlist:filePath];
            }
        }
    }
}

- (void)sendAppStoreRequestBuyPlist:(NSString *)plistPath{
    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:plistPath];
    //这里的参数请根据自己公司后台服务器接口定制,但是必须发送的是持久化保存购买凭证
    NSData *receipt = [dic objectForKey:IAP_RECEIPT];
    if(receipt==nil){
        [self sendAppStoreRequestSucceededWithData:plistPath];
        return;
    }
    NSString *receiptBase64 = [NSString base64StringFromData:receipt length:[receipt length]];
    NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
    [requestDict setObject:[dic objectForKey:IAP_USER_ID] forKey:@"userid"];
    [requestDict setObject:[CommonMethod paramStringIsNull:receiptBase64] forKey:@"data"];
    [[[UIViewController alloc] init] requstType:RequestType_Post apiName:API_NAME_USER_POST_USER_IAPSECONDVALID paramDict:requestDict hud:nil success:^(AFHTTPRequestOperation *operation, id responseObject, MBProgressHUD *hud) {
        if([CommonMethod isHttpResponseSuccess:responseObject] == HttpResponseTypeSuccess){
            [self sendAppStoreRequestSucceededWithData:plistPath];
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error, MBProgressHUD *hud) {
    }];
}

//验证成功就从plist中移除凭证
- (void)sendAppStoreRequestSucceededWithData:(NSString *)plistPath{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:plistPath]){
        [fileManager removeItemAtPath:plistPath error:nil];
    }
}




2018-06-28 17:58:28 qcx321 阅读数 4697

在iOS开发中如果涉及到虚拟物品的购买,就需要使用IAP服务,我们今天来看看如何实现。

在实现代码之前我们先做一些准备工作,一步步来看。

1、IAP流程

IAP流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另一种就是自己假设服务器进行验证。由于国内网络连接Apple服务器验证非常慢,而且也为了防止黑客伪造购买凭证,通用做法是自己架设服务器进行验证。

下面我们通过图来看看两种方式的差别:

1.1、使用Apple服务器

这里写图片描述

1.2、自己架设服务器

这里写图片描述

简单说下第二中情况的流程:

1.用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户

2.用户点击购买购买某一个虚拟物品,APP就发送该虚拟物品的productionIdentifier到Apple服务器

3.Apple服务器根据APP发送过来的productionIdentifier返回相应的物品的信息(描述,价格等)

4.用户点击确认键购买该物品,购买请求发送到Apple服务器

5.Apple服务器完成购买后,返回用户一个完成购买的凭证

6.APP发送这个凭证到后台服务器验证

7.后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效

8.后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理

2、iTunes Connet操作

搞清楚了自己架设服务器是如何完成IAP购买的流程了之后,我们下一步就是登录到iTunes Connet创建应用和指定虚拟物品价格表

2.1、创建自己的App

如下图所示,我们需要创建一个自己的APP,要注意的是这里的Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致。也就是图中红框部分。

这里写图片描述

2.2、创建虚拟物品价格表

2.2.1、虚拟物品分为如下几种:

1.消耗品(Consumable products):比如游戏内金币等。

2.不可消耗品(Non-consumable products):简单来说就是一次购买,终身可用(用户可随时从App Store restore)。

3.自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅(如果用户同意)。

4.非自动更新订阅品(Non-renewable subscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差异。

5.免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过期。只能用于Newsstand-enabled apps。

类型2、3、5都是以Apple ID为粒度的。比如小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上都可以使用。

类型1、4一般来说则是现买现用。如果开发者自己想做更多控制,一般选4

2.2.2、创建成功后如下所示:

这里写图片描述

其中产品id是字母或者数字,或者两者的组合,用于唯一表示该虚拟物品,app也是通过请求产品id来从apple服务器获取虚拟物品信息的。

2.3、设置税务和银行卡信息

这一步必须设置,不然是无法从apple获取虚拟产品信息。

设置成功后如下所示:

这里写图片描述


3、iOS端具体代码实现

完成了上面的准备工作,我们就可以开始着手IAP的代码实现了。

我们假设你已经完成了从后台服务器获取虚拟物品列表这一步操作了,这一步后台服务器还会返回每个虚拟物品所对应的productionIdentifier,假设你也获取到了,并保存在属性self.productIdent中。

需要在工程中引入 storekit.framework

我们来看看后续如何实现IAP

3.1、确认用户是否允许IAP

//移除监听
-(void)dealloc
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

//添加监听
- (void)viewDidLoad{
    [super viewDidLoad];
    [self.tableView.mj_header beginRefreshing];
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

- (void)buyProdution:(UIButton *)sender{    
    if ([SKPaymentQueue canMakePayments]) {
        [self getProductInfo:self.productIdent];
    } else {
        [self showMessage:@"用户禁止应用内付费购买"];
    }
}

3.2、发起购买操作

如果用户允许IAP,那么就可以发起购买操作了

//从Apple查询用户点击购买的产品的信息
- (void)getProductInfo:(NSString *)productIdentifier {
    NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
    NSSet *set = [NSSet setWithArray:product];
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
    [self showMessageManualHide:@"正在购买,请稍后"];
}

// 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    [self hideHUD];
    NSArray *myProduct = response.products;
    if (myProduct.count == 0) {
        [self showMessage:@"无法获取产品信息,请重试"];
        return;
    }
    SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

//查询失败后的回调
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    [self hideHUD];
    [self showMessage:[error localizedDescription]];
}

3.3、购买操作后的回调

//购买操作后的回调
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    [self hideHUD];
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                self.receipt = [GTMBase64 stringByEncodingData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]];
                [self checkReceiptIsValid];//把self.receipt发送到服务器验证是否有效
                [self completeTransaction:transaction];
                break;

            case SKPaymentTransactionStateFailed://交易失败
                [self failedTransaction:transaction];
                break;

            case SKPaymentTransactionStateRestored://已经购买过该商品
                [self showMessage:@"恢复购买成功"];
                [self restoreTransaction:transaction];
                break;

            case SKPaymentTransactionStatePurchasing://商品添加进列表
                [self showMessage:@"正在请求付费信息,请稍后"];
                break;

            default:
                break;
        }
    }

}



- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}


- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if(transaction.error.code != SKErrorPaymentCancelled) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];
        [alertView show];
    } else {
        [self showMessage:@"用户取消交易"];
    }

    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}


- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

3.4、向服务器端验证购买凭证的有效性

在这一步我们需要向服务器验证Apple服务器返回的购买凭证的有效性,然后把验证结果通知用户

- (void)checkReceiptIsValid{

    AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters::@"发送的参数(必须包括购买凭证)"
    success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
        if(凭证有效){
          你要做的事
        }else{//凭证无效
          你要做的事
        }

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];
            [alertView show];
    }

}

3.5、发送凭证失败的处理

如果出现网络问题,导致无法验证。我们需要持久化保存购买凭证,在用户下次启动APP的时候在后台向服务器再一次发起验证,直到成功然后移除该凭证。
保证如下define可在全局访问:

#define AppStoreInfoLocalFilePath [NSString stringWithFormat:@"%@/%@/", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],@"EACEF35FE363A75A"]
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 0)
    {
        [self saveReceipt];
    }
    else
    {
        [self checkReceiptIsValid];
    }
}

//AppUtils 类的方法,每次调用该方法都生成一个新的UUID
+ (NSString *)getUUIDString
{
    CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
    CFStringRef strRef = CFUUIDCreateString(kCFAllocatorDefault , uuidRef);
    NSString *uuidString = [(__bridge NSString*)strRef stringByReplacingOccurrencesOfString:@"-" withString:@""];
    CFRelease(strRef);
    CFRelease(uuidRef);
    return uuidString;
}

//持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不同的凭证)
-(void)saveReceipt{
    NSString *fileName = [AppUtils getUUIDString];
    NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName];

    NSDictionary *dic =[ NSDictionary dictionaryWithObjectsAndKeys:
                        self.receipt,                           Request_transactionReceipt,
                        self.date                               DATE                        
                        self.userId                             USERID
                        nil];

    [dic writeToFile:savedPath atomically:YES];
}

3.6、APP启动后再次发送持久化存储的购买凭证到后台服务器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    
    NSFileManager *fileManager = [NSFileManager defaultManager];

    //从服务器验证receipt失败之后,在程序再次启动的时候,使用保存的receipt再次到服务器验证
    if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//如果在改路下不存在文件,说明就没有保存验证失败后的购买凭证,也就是说发送凭证成功。
        [fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//创建目录
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:nil];
    }
    else//存在购买凭证,说明发送凭证失败,再次发起验证
    {
        [self sendFailedIapFiles];
    }
}

//验证receipt失败,App启动后再次验证
- (void)sendFailedIapFiles{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;

    //搜索该目录下的所有文件和目录
    NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error];

    if (error == nil)
    {
        for (NSString *name in cacheFileNameArray)
        {
            if ([name hasSuffix:@".plist"])//如果有plist后缀的文件,说明就是存储的购买凭证
            {
                NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name];
                [self sendAppStoreRequestBuyPlist:filePath];

            }
        }
    }
    else
    {
        DebugLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]);
    }
}

-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath
{
    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:plistPath];

    //这里的参数请根据自己公司后台服务器接口定制,但是必须发送的是持久化保存购买凭证
    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
              [dic objectForKey:USERID],                           USERID,                    
              [dic objectForKey:DATE],                             DATE,  
              [dic objectForKey:Receipt],                            Receipt,                                                                             
              nil];


        AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters:params  success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
        if(凭证有效){
         [self removeReceipt]
        }else{//凭证无效
          你要做的事
        }

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

    }

 }

//验证成功就从plist中移除凭证
-(void)sendAppStoreRequestSucceededWithData
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:AppStoreInfoLocalFilePath])
    {
        [fileManager removeItemAtPath:AppStoreInfoLocalFilePath error:nil];
    }
}

4、测试

创建应用的沙盒测试账号

这里写图片描述

点击+号,根据具体信息填写。

至此,整个流程结束。


注: 完成购买的操作中,如果有服务器,需要向服务器验证购买结果,如果没有,就直接完成。

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
调起支付后,输入注册的沙盒账号即可点击购买。

注:这里有几个注意事项

一,测试支付的ipa必须使用[App-Store]证书

二,越狱机器无法测试IAP

三,用SandBox账号测试支付的时候,必须把在系统[设置]里面把[Itunes Store 与 App Store]登录的非                               SandBox账号注销掉,否则向苹果服务器请求不到订单信息

四,Sandbox账号不要在正式支付环境登陆支付,登陆过的正式支付环境的SandBox账号会失效

五,所有在itunes上配置的商品都必须可购买,不能有某些商品根据商户自己的服务器的数据在某个时期出现免费的情况

六,商品列表不能按照某些特定条件进行排序(比如说下载量)

七,非消耗型商品必须的有恢复商品功能

八,非消耗类型的商品不要和商户自己的服务器关联

参考:

https://www.jianshu.com/p/033086546126

https://www.jianshu.com/p/e0ea5b8916f5

http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/

http://www.himigame.com/iphone-cocos2d/550.html

http://blog.devtang.com/2012/12/09/in-app-purchase-check-list/

http://yarin.blog.51cto.com/1130898/549141

更多技术文章,欢迎大家访问我的技术博客:https://blog.csdn.net/qcx321

2017-05-31 15:40:18 swj524152416 阅读数 9630

Unity iOS内购

内购流程

  • 1、在 AppStore 中创建相应的物品,创建内购沙盒测试账号
  • 2、客户端从后台获取相应的物品 ID (当然也可以再客户端写死,但后期扩展性就受限制了)
  • 3、依据相应的物品 ID 请求商品的相关信息
  • 4、依据商品信息创建订单请求交易
  • 5、依据返回的订单状态处理交易结果
  • 6、请求后台再次验证订单状态
  • 7、依据后台返回结果处理相关逻辑

2、创建内购物品以及沙盒测试账号


思路:

Unity调用iOS内购代码实现

效果图:

购买弹框
购买结果

重要提示:

测试一定要用沙盒账号,否则无效!

流程

这里就不重复写了,直接上截图 
这里写图片描述

OC代码:

IAPInterface(主要是实现Unity跟OC的IAP代码的一个交互作用,等于是一个中间桥梁)

#import <Foundation/Foundation.h>

@interface IAPInterface : NSObject

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
#import "IAPInterface.h"
#import "IAPManager.h"

@implementation IAPInterface

void TestMsg(){
    NSLog(@"Msg received");

}

void TestSendString(void *p){
    NSString *list = [NSString stringWithUTF8String:p];
    NSArray *listItems = [list componentsSeparatedByString:@"\t"];

    for (int i =0; i<listItems.count; i++) {
        NSLog(@"msg %d : %@",i,listItems[i]);
    }

}

void TestGetString(){
    NSArray *test = [NSArray arrayWithObjects:@"t1",@"t2",@"t3", nil];
    NSString *join = [test componentsJoinedByString:@"\n"];


    UnitySendMessage("Main", "IOSToU", [join UTF8String]);
}

IAPManager *iapManager = nil;

void InitIAPManager(){
    iapManager = [[IAPManager alloc] init];
    [iapManager attachObserver];

}

bool IsProductAvailable(){
    return [iapManager CanMakePayment];
}

void RequstProductInfo(void *p){
    NSString *list = [NSString stringWithUTF8String:p];
    NSLog(@"productKey:%@",list);
    [iapManager requestProductData:list];
}

void BuyProduct(void *p){
    [iapManager buyRequest:[NSString stringWithUTF8String:p]];
}

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

IAPManager(真真的iOS的购买功能)

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface IAPManager : NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>{
    SKProduct *proUpgradeProduct;
    SKProductsRequest *productsRequest;
}

-(void)attachObserver;
-(BOOL)CanMakePayment;
-(void)requestProductData:(NSString *)productIdentifiers;
-(void)buyRequest:(NSString *)productIdentifier;

@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
#import "IAPManager.h"

@implementation IAPManager

-(void) attachObserver{
    NSLog(@"AttachObserver");
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

-(BOOL) CanMakePayment{
    return [SKPaymentQueue canMakePayments];
}

-(void) requestProductData:(NSString *)productIdentifiers{
    NSArray *idArray = [productIdentifiers componentsSeparatedByString:@"\t"];
    NSSet *idSet = [NSSet setWithArray:idArray];
    [self sendRequest:idSet];
}

-(void)sendRequest:(NSSet *)idSet{
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:idSet];
    request.delegate = self;
    [request start];
}

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *products = response.products;

    for (SKProduct *p in products) {
        UnitySendMessage("Main", "ShowProductList", [[self productInfo:p] UTF8String]);
    }

    for(NSString *invalidProductId in response.invalidProductIdentifiers){
        NSLog(@"Invalid product id:%@",invalidProductId);
    }

    [request autorelease];
}

-(void)buyRequest:(NSString *)productIdentifier{
    SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

-(NSString *)productInfo:(SKProduct *)product{
    NSArray *info = [NSArray arrayWithObjects:product.localizedTitle,product.localizedDescription,product.price,product.productIdentifier, nil];

    return [info componentsJoinedByString:@"\t"];
}

-(NSString *)transactionInfo:(SKPaymentTransaction *)transaction{

    return [self encode:(uint8_t *)transaction.transactionReceipt.bytes length:transaction.transactionReceipt.length];

    //return [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSASCIIStringEncoding];
}

-(NSString *)encode:(const uint8_t *)input length:(NSInteger) length{
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    NSMutableData *data = [NSMutableData dataWithLength:((length+2)/3)*4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for(NSInteger i=0; i<length; i+=3){
        NSInteger value = 0;
        for (NSInteger j= i; j<(i+3); j++) {
            value<<=8;

            if(j<length){
                value |=(0xff & input[j]);
            }
        }

        NSInteger index = (i/3)*4;
        output[index + 0] = table[(value>>18) & 0x3f];
        output[index + 1] = table[(value>>12) & 0x3f];
        output[index + 2] = (i+1)<length ? table[(value>>6) & 0x3f] : '=';
        output[index + 3] = (i+2)<length ? table[(value>>0) & 0x3f] : '=';
    }

    return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}

-(void) provideContent:(SKPaymentTransaction *)transaction{
    UnitySendMessage("Main", "ProvideContent", [[self transactionInfo:transaction] UTF8String]);
}

-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            default:
                break;
        }
    }
}

-(void) completeTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Comblete transaction : %@",transaction.transactionIdentifier);
    [self provideContent:transaction];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

-(void) failedTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Failed transaction : %@",transaction.transactionIdentifier);

    if (transaction.error.code != SKErrorPaymentCancelled) {
        NSLog(@"!Cancelled");
    }
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

-(void) restoreTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"Restore transaction : %@",transaction.transactionIdentifier);
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


@end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

Unity中调用的C#代码

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public class IAPExample : MonoBehaviour {

    public List<string> productInfo = new List<string>();

    [DllImport("__Internal")]
    private static extern void TestMsg();//测试信息发送

    [DllImport("__Internal")]
    private static extern void TestSendString(string s);//测试发送字符串

    [DllImport("__Internal")]
    private static extern void TestGetString();//测试接收字符串

    [DllImport("__Internal")]
    private static extern void InitIAPManager();//初始化

    [DllImport("__Internal")]
    private static extern bool IsProductAvailable();//判断是否可以购买

    [DllImport("__Internal")]
    private static extern void RequstProductInfo(string s);//获取商品信息

    [DllImport("__Internal")]
    private static extern void BuyProduct(string s);//购买商品

    //测试从xcode接收到的字符串
    void IOSToU(string s){
        Debug.Log ("[MsgFrom ios]"+s);
    }

    //获取product列表
    void ShowProductList(string s){
        productInfo.Add (s);
    }
    bool back = false;
    //获取商品回执
    void ProvideContent(string s){
        Debug.Log ("[MsgFrom ios]proivideContent : "+s);
        back = true;
    }


    // Use this for initialization
    void Start () {
        InitIAPManager();
    }

    void OnGUI(){

        if(Btn ("GetProducts")){
            if(!IsProductAvailable())
                throw new System.Exception("IAP not enabled");
            productInfo = new List<string>();
            RequstProductInfo("com.aladdin.fishpocker1\tcom.aladdin.fishpocker2");
        }

        GUILayout.Space(40);

        if (back)
            GUI.Label (new Rect (10, 150, 100, 100), "Message back");

        for(int i=0; i<productInfo.Count; i++){
            if(GUILayout.Button (productInfo[i],GUILayout.Height (100), GUILayout.MinWidth (200))){
                string[] cell = productInfo[i].Split('\t');
                Debug.Log ("[Buy]"+cell[cell.Length-1]);
                BuyProduct(cell[cell.Length-1]);
                GUI.Label(new Rect (10, 10, 100, 200), string.Format("[Buy]{0}" ,cell[cell.Length-1]));
            }  
        }
    }

    bool Btn(string msg){
        GUILayout.Space (100);
        return  GUILayout.Button (msg,GUILayout.Width (200),GUILayout.Height(100));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

Git地址点击下载

在这里需要注意几点,

  1. 代码中的_currentProId所填写的是你的购买项目的的ID,这个和第二步创建的内购的productID要一致;本例中是 123。

  2. 在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。

  3. 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。

  4. 请务必使用真机来测试,一切以真机为准。

  5. 项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。

  6. 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号

  7. 二次验证,请注意区分宏, 测试用沙盒验证,App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。

附:苹果支付错误目录


如果直接使用上面的代码需要注意的是场景中这个脚本挂载的对象名必须为Main,当然你也可以在IAPManager中修改Main为其他你要挂载的对象名
IAPManager中有个UnitySendMessage("Main",。。。。)方法负责接收ios的回执函数并在Unity中回执调用。

沙盒测试内购功能须先退出iTunes store的id然后直接登录游戏 在游戏内登录申请的沙盒账号,然后测试内购功能(审核显示等待APP提交状态中的内购也可以使用沙箱测试)

沙箱账号请求验证的苹果服务器地址和正常发布购买的地址不同见下文

iOS 内购验证

如果我们不做任何处理的话,越狱机是可以直接绕过支付验证直接获得结果的,这样对于我们辛辛苦苦的开发者来说简直噩耗,所以我们有必要了解一下内购验证相关的知识,以及知道如何去预防这样的事情。

校验文章

http://www.cnblogs.com/zhaoqingqing/p/4597794.html

相关资料

本地验证:

优点:

  • 无需服务器验证

缺点:

  • 项目里需要引入 OpenSSL

链接:

服务器验证:

优点:

  • server-side verification over SSL is the most reliable way to determine the authenticity of purchasing records

缺点:

  • 需要部署服务器,服务器和 App 之间的数据交换可能更容易被破解

链接:

双重验证:

先本地验证一次,后服务器再验证一次(感觉没必要)

其他:

常见的破解方法:

总的来说:

  • 服务器验证更适合有自己账号系统的 App,直接可以对 IAP 破解免疫,否则一样很简单就被破解
  • 本地验证使用下面的方法来增强验证 
    • Check that the SSL certificate used to connect to the App Store server is an EV certificate.
    • Check that the information returned from validation matches the information in the SKPayment object.
    • Check that the receipt has a valid signature.
    • Check that new transactions have a unique transaction ID.