【Flutter】App内购支付集成 Google和Apple支付和服务器验证全流程

Flutter支付集成

前言:

以谷歌内购为例,我们需要做的总共为三步

  1. 需要在谷歌市场配置商品,设置测试渠道,配置开发者账号,设置对应权限。
  2. 配置完商品之后,如何在 Flutter 中获取到商品,购买指定商品,消耗商品等。
  3. 购买成功之后,如何到服务器校验是否支付成功,后台服务器如何配置通行权限,谷歌市场与谷歌云的关联以及相关校验。

购买交易的生命周期
下面是一次性购买或订阅的典型购买流程:

  1. 向用户展示他们可以购买什么。
  2. 启动购买流程,以便用户接受购买交易。
  3. 在您的服务器上验证购买交易。
  4. 向用户提供内容。
  5. 确认内容已传送给用户。对于消耗型商品,用户要先消耗掉已购商品,才能再次购买。

订阅会自动续订,直到被取消。订阅可处于下面这几种状态:

  • 有效:用户信誉良好,可享用订阅内容。
  • 已取消:用户已取消订阅,但在到期前仍可享用订阅内容。
  • 处于宽限期:用户遇到了付款问题,但仍可享用订阅内容,同时 Google 会重新尝试通过相应的付款方式扣款。
  • 暂时保留:用户遇到了付款问题,不能再享用订阅内容,同时 Google 会重新尝试通过相应的付款方式扣款。
  • 已暂停:用户暂停了其订阅,在恢复之前不能享用订阅内容。
  • 已到期:用户已取消且不能再享用订阅内容。用户在订阅到期时会被视为流失。

支付流程示意图

在这里插入图片描述

一 、google开发者平台配置

首先进入谷歌开发者平台
https://developers.google.com/?hl=zh-cn

进入开发者平台之后,点击google play,创建我们的APP
在这里插入图片描述

点击登录管理中心
在这里插入图片描述

创建完我们的APP之后,就可以开始配置支付的功能。需要注意的是,在进行谷歌支付测试的时候,需要先提交一个封闭测试版本及以上等级(例如公开版本)的包,然后才可以去创建应用内支付的商品,等这个包提交审核通过之后才可以开始进行谷歌支付的测试。

1.1、创建定价模板

在设置页面
在这里插入图片描述

找到付款概况之后,如果没有付款账号,我们填写一些信息,姓名,邮箱,账号,等等信息,创建完成之后我们就可以设置定价的模板。
如果能创建模板说明你付款账号没问题,定价模板是非必须的,可有可无,但是定义了模板之后会更加方便,到时候创建商品可以直接关联模板,账号下的每一个子应用的内购商品都能关联对应的模板,有一个统一的定价。
如何创建定价模板如下:
在这里插入图片描述

我们创建模板之后,就可以定义模板的价格与标题,选择的金额会有对应的汇率转换,比如我创建的新加坡币,如果用港元支付的话,会根据汇率转换为对应的港元支付。
在这里插入图片描述

创建完成之后,我们就能看到对应的定价模板如下图所示:
在这里插入图片描述

1.2、上架封闭测试App

点击创建轨道
在这里插入图片描述

点击创建新的发布版本
在这里插入图片描述

签名选择Google管理签名,然后上传aab格式的release版本的包,aab版本的包在这里生成
点开Build,选择Generate Signed Bundle/APK
在这里插入图片描述

然后选择app bundle
在这里插入图片描述

然后一路next,最后选择release版本,然后finish
在这里插入图片描述

然后在输出控制台的build选项卡,即可找到刚刚打出来的aab包
在这里插入图片描述

然后上传就可以了。

1.3、创建应用内购商品

此时就可以配置应用内商品了,点击这里进行添加配置:
在这里插入图片描述

添加完成后记得激活,不然即使审核通过之后测试的时候也获取不到该商品

点击这里激活商品
在这里插入图片描述

这个时候商品的配置就完成了。

接下来添加测试账户,进入封闭测试页面,切换到【测试用户选项卡】,然后创建测试群组,在群组里添加测试人员账户即可
在这里插入图片描述

当你的APP审核通过之后,这个页面下方的测试人员参与方式便会生效,如下所示:
在这里插入图片描述

就可以将这些链接发给测试人员,让他们去安装进行测试购买。

最后修改一下测试政策状态
在这里插入图片描述

选中测试群组,然后将政策状态改为LICENSED
在这里插入图片描述

OK,配置完成

二 、Apple开发者平台添加内购商品

首先使用苹果开发者账户登录苹果开发者平台

https://developer.apple.com/account

点击【App】
在这里插入图片描述

添加新的苹果内购商品
在这里插入图片描述

添加的时候页面的指引很清晰,就不赘述了,苹果添加内购商品比较简单,加完就可以了。

然后去创建沙盒账户用来做苹果支付测试,回到首页,点击【用户和访问】
在这里插入图片描述

点击沙盒,然后添加一个苹果测试账户,这个账户可以是个假的邮箱,不需要是正式的Apple id,比如你可以设置为8888888@qq.com类似之类的账户
在这里插入图片描述

添加完点击创建即可
在这里插入图片描述

OK,配置完成

三、flutter 代码集成

使用到的官方推出的应用内购插件:

in_app_purchase: ^3.2.0

插件官网地址:https://pub.dev/packages/in_app_purchase

使用起来并不复杂,可以说是 Android 与 iOS 的逻辑是一样样的。

将插件添加至yaml文件,然后执行flutter pub get
在这里插入图片描述

执行完了记得去IOS和安卓端分别执行pod install 和 gradle sync同步一下第三方插件

然后在项目中新建dart文件,命名为:BuyEngine.dart

然后将以下代码放入:

import 'dart:async';
import 'dart:io';
 
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
 
class BuyEngin{
 
  StreamSubscription<List<PurchaseDetails>> _subscription;
  InAppPurchase _inAppPurchase;
  List<ProductDetails> _products; //内购的商品对象集合
 
  //初始化购买组件
  void initializeInAppPurchase() {
 
    // 初始化in_app_purchase插件
    _inAppPurchase = InAppPurchase.instance;
 
    //监听购买的事件
    final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
    _subscription = purchaseUpdated.listen((purchaseDetailsList) {
      _listenToPurchaseUpdated(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (error) {
      error.printError();
      print("购买失败了");
    });
  }
 
  void resumePurchase(){
    _inAppPurchase.restorePurchases();
  }
 
  /// 加载全部的商品
  void buyProduct(String productId) async {
 
    print("请求商品id " + productId);
 
    List<String> _outProducts = [productId];
 
    final bool available = await _inAppPurchase.isAvailable();
    if (!available) {
      // ToastUtil.showToast("无法连接到商店");
      print("无法连接到商店");
      return;
    }
 
    //开始购买
    // ToastUtil.showToast("连接成功-开始查询全部商品");
    print("连接成功-开始查询全部商品");
    List<String> _kIds = _outProducts;
 
    final ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(_kIds.toSet());
    print("商品获取结果  " + response.productDetails.toString());
    if (response.notFoundIDs.isNotEmpty) {
      // ToastUtil.showToast("无法找到指定的商品");
      print("无法找到指定的商品");
      // ToastUtil.showToast("无法找到指定的商品 数量 " + response.productDetails.length.toString());
 
      return;
    }
 
    // 处理查询到的商品列表
    List<ProductDetails> products = response.productDetails;
    print("products ==== " + products.length.toString());
    if (products.isNotEmpty) {
      //赋值内购商品集合
      _products = products;
    }
 
    print("全部商品加载完成了,可以启动购买了,总共商品数量为:${products.length}");
 
    //先恢复可重复购买
    // await _inAppPurchase. ();
 
    startPurchase(productId);
  }
 
 
  // 调用此函数以启动购买过程
  void startPurchase(String productId) async {
 
    print("购买的商品id为" + productId);
    if (_products != null && _products.isNotEmpty) {
      // ToastUtil.showToast("准备开始启动购买流程");
      try {
        ProductDetails productDetails = _getProduct(productId);
 
        print("一切正常,开始购买,信息如下:title: ${productDetails.title}  desc:${productDetails.description} "
            "price:${productDetails.price}  currencyCode:${productDetails.currencyCode}  currencySymbol:${productDetails.currencySymbol}");
        _inAppPurchase.buyConsumable(purchaseParam: PurchaseParam(productDetails: productDetails));
      } catch (e) {
        e.printError();
        print("购买失败了");
      }
    } else {
      print("当前没有商品无法调用购买逻辑");
    }
  }
 
  // 根据产品ID获取产品信息
  ProductDetails _getProduct(String productId) {
    return _products.firstWhere((product) => product.id == productId);
  }
 
  /// 内购的购买更新监听
  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
    for (PurchaseDetails purchase in purchaseDetailsList) {
      if (purchase.status == PurchaseStatus.pending) {
        // 等待支付完成
        _handlePending();
      } else if (purchase.status == PurchaseStatus.canceled) {
        // 取消支付
        _handleCancel(purchase);
      } else if (purchase.status == PurchaseStatus.error) {
        // 购买失败
        _handleError(purchase.error);
      } else if (purchase.status == PurchaseStatus.purchased || purchase.status == PurchaseStatus.restored) {
        // ToastUtil.showToast(DataConfig.getShowName("Pay_Success_Tip"));
        //完成购买, 到服务器验证
        if (Platform.isAndroid) {
          var googleDetail = purchase as GooglePlayPurchaseDetails;
          checkAndroidPayInfo(googleDetail);
        } else if (Platform.isIOS) {
          var appstoreDetail = purchase as AppStorePurchaseDetails;
          checkApplePayInfo(appstoreDetail);
        }
      }
    }
  }
 
  /// 购买失败
  void _handleError(IAPError iapError) {
    // ToastUtil.showToast("${DataConfig.getShowName("Purchase_Failed")}:${iapError?.code} message${iapError?.message}");
  }
 
  /// 等待支付
  void _handlePending() {
    print("等待支付");
  }
 
  /// 取消支付
  void _handleCancel(PurchaseDetails purchase) {
    _inAppPurchase.completePurchase(purchase);
  }
 
  /// Android支付成功的校验
  void checkAndroidPayInfo(GooglePlayPurchaseDetails googleDetail) async {
    _inAppPurchase.completePurchase(googleDetail);
    print("安卓支付交易ID为" + googleDetail.purchaseID);
    print("安卓支付验证收据为" + googleDetail.verificationData.serverVerificationData);
  }
 
  /// Apple支付成功的校验
  void  checkApplePayInfo(AppStorePurchaseDetails appstoreDetail) async {
    _inAppPurchase.completePurchase(appstoreDetail);
 
    print("Apple支付交易ID为" + appstoreDetail.purchaseID);
    print("Apple支付验证收据为" + appstoreDetail.verificationData.serverVerificationData);
  }
 
 
  @override
  void onClose() {
    if (Platform.isIOS) {
      final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
      _inAppPurchase.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
      iosPlatformAddition.setDelegate(null);
    }
    _subscription.cancel();
  }
 
}

至此集成完毕,开始测试谷歌支付

三 、支付测试

在调用支付的地方提前初始化购买插件:

BuyEngin _buyEngin = BuyEngin();
_buyEngin.initializeInAppPurchase();

然后调用即可:

_buyEngin.buyProduct("应用内商品ID");

应用内商品ID就是你在google开发者中心或APP Store Connect 配置的应用内购买商品的product ID

如果一切正常,则会正常唤醒谷歌或苹果支付
在这里插入图片描述

支付完成后可以看到可以正常获取到交易的ID和交易的验证收据,为了避免被第三方恶意刷购买接口来进行非法购买,建议将该收据上传后端服务器进行验证,验证通过之后再去更新用户的购买信息。
在这里插入图片描述

Ok ,集成完毕,功德+1

四、 服务器校验相关流程

为什么要加后端校验?客户端支付成功了,服务端怎么知道,万一用接口的方式通信,如果被抓包岂不是可以无限加金币了。太不安全了,所以才有服务器校验这一步。

iOS的校验不用说,很简单,拿到支付完成的票据直接发起请求即可,而 Android 的服务端校验就相对麻烦,需要配置谷歌云,以及对应的通行权限。

谷歌结算文档:https://developer.android.com/google/play/billing?hl=zh-cn
谷歌支付校验AI:https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products?hl=zh-cn
如果我们直接在API中调用校验接口,那肯定是直接报错:

{
  "error": {
    "code": 403,
    "message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.",
    "errors": [
      {
        "message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.",
        "domain": "androidpublisher",
        "reason": "projectNotLinked"
      }
    ]
  }
}


没有授权,接下来开始授权

4.1、Google Cloud关联

首先需要配置 Google Cloud 并且配置相关的账号,对应指定的应用。
点击项目的 API Access 中
在这里插入图片描述

如果这一步你没有 Google Cloud 账号,可以创建或关联已有的 Google Cloud 账号,这里我没有就直接创建了Google Cloud 账号。关联之后我们就能看到上图所示的画面。

我们可以直接在谷歌市场控制台中的 API Access 中直接进入谷歌云后台,也能 直接输入网址 https://code.google.com/apis/console/ 是一样的效果。

我们关联 Google Cloud 账号之后,默认就已经开通 Google Play Developer API 权限。
在这里插入图片描述

所以我们不需要再次去授权了。
在这里插入图片描述

如果觉得不保险,也能在里面搜索 Billing ,然后启动相关的支付服务权限

4.2 、创建 web-OAuth 授权

当我们在谷歌市场的后台关联谷歌云的时候,就已经帮我们初始化了很多配置,已经都有了
我们再谷歌云后台,在APIs & auth 项中找到 Credentials,直接查看即可:
在这里插入图片描述

我们点击 Web 授权进去配置相关配置。
主要是配置左侧的上下两个 URI 地址,上面的配置后台域名:
在这里插入图片描述

下面的是固定写法,callback的地址一定是可用域名 + /oauth2callback。
在这里插入图片描述

创建完成之后,记得记录你的三个重要字段,client_id 和 client_secret 以及 redirect_uri ,后面会用到。
通过访问一下的网页获取到一个oauth2callback:

https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&
redirect_uri=https://api.whatsapp.sg/oauth2callback&client_id=816630003638-5p27m684jfpfa6sh6l9chbpreq2hg9ov.apps.googleusercontent.com 

返回一个code:

https://api.whatsapp.sg/oauth2callback?code=4/CpVOd8CljO_gxTRE1M5jtwEFwf8gRD44vrmKNDi4GSS.kr-GHuseD-oZEnp6UADFXm0E0MD3FlAI

拿到后面的 code 字段。

code=4/CpVOd8CljO_gxTRE1M5jtwEFwf8gRD44vrmKNDi4GSS.kr-GHuseD-oZEnp6UADFXm0E0MD3FlAI

我们手动的在 postman 之类的工具上,通过固定的参数,拿到 refresh_token(重点,后期全靠它)

{
        'grant_type':'authorization_code',
        'code':'4/CpVOd8CljO_gxTRE1M5jtwEFwf8gRD44vrmKNDi4GSS.kr-GHuseD-oZEnp6UADFXm0E0MD3FlAI',//上一步获取的,
        'client_id':'816630003638-5p27m684jfpfa6sh6l9chbpreq2hg9ov.apps.googleusercontent.com',
        'client_secret':'36WnPnojshgj56uhghj-xCo',
        'redirect_uri':'https://api.whatsapp.sg/oauth2callback',
}
    


向以下的网址发起 Post 请求。

https://accounts.google.com/o/oauth2/token

一定要保证网络畅通,只有一次机会,返回的json对象如下

{
"access_token" : "",
"token_type" : "Bearer",
"expires_in" : 3600,
"refresh_token" : "1/zaaHNytlC3SEBX7F2cfrHcqJEa3KoAHYeXES6nmho"
}

refresh_token 就拿到了,注意一定要保存好,只有这一次机会,如果再次调用此接口 refresh_token 就是空了,不会返回了。

4.3、web-OAuth校验支付是否成功

拿到这个refresh_token就可以调用真正的校验接口了,例如我们后端调用的是否支付成功:

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{purchaseToken}?access_token={$access_token}"


这里的packageName,productId,purchaseToken 大家都很熟悉了,就是Android 支付成功之后返回给我们的,直接传递给后端即可,而access_token其实就是我们上面拿到的 refresh_token。

我们需要拿到第一次返回的 refresh_token 保存起来,后续以刷新的方式来获取新的 refresh_token ,用于访问真正的API。

后台调用验证接口完成之后得到的对象如下:

{
  "kind": string,
  "purchaseTimeMillis": string,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer,
  "acknowledgementState": integer,
  "purchaseToken": string,
  "productId": string,
  "quantity": integer,
  "obfuscatedExternalAccountId": string,
  "obfuscatedExternalProfileId": string,
  "regionCode": string
}

只需要验证状态即可:

consumptionState == 0 purchaseState == 0

说明这个商品已经购买了,并且也没有被消耗,那么此时就可以给移动端返回true,让移动端执行消耗操作。
后端PHP的校验谷歌内购是否成功示例代码:

   public function checkGooglePay(){  
         $google_public_key    = "你的公钥(google后台在你的应用下获取)";  
         $inapp_purchase_data  = $_REQUEST['signtureTemp'];   
         $inapp_data_signature = $_REQUEST['signtureDataTemp'];   
          $key        = "-----BEGIN PUBLIC KEY-----\n".chunk_split($google_public_key, 64,"\n").'-----END PUBLIC KEY-----';  
          $key        = openssl_pkey_get_public($key);   
          $signature  = base64_decode($inapp_data_signature);  
          $ok         = openssl_verify($inapp_purchase_data,$signature,$key,OPENSSL_ALGO_SHA1);      
          if (1 == $ok) {  
             // 支付验证成功!   
             //进行二次验证,订单查询     
             
           // 1.获取access_token(3600秒有效期)
             $access_token_url = "https://accounts.google.com/o/oauth2/token";
            $data_tmp2 = array(
                 'grant_type'=>'refresh_token',
                 'refresh_token'=>'',//长效token
                 'client_id'=>'',    //客户端id    
                'client_secret'=>'',//客户端密钥
                 );
             $http = new http($access_token_url,'POST',5);
             $http->setContent($data_tmp2);
             $result = $http->exec();
            $result = json_decode($contents,true);
             $access_token = $result['access_token'];

             //2.通过获得access_token 就可以请求谷歌的API接口,获得订单状态
             $packageName=""//包名
            $productId="" //产品Id
             $purchaseToken=""
        
              $url = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{purchaseToken}?access_token={$access_token}";
               $http = new http($url,'GET',5);
             $http->setContent($data);
             $contents = $http->exec();
             $contents = json_decode($contents,true);
              
            if($contents['consumptionState'] == 0 && $contents['purchaseState'] == 0){
                //验证成功  购买成功并且没有消耗  google支付中客户端如果没有进行消耗是不能再次购买该商品
                 //处理游戏逻辑 发钻石,通知客户端进行消耗
             }else{
                 //订单验证失败
             }             
         }else{  
             //签名验证失败
 
         }           
     }

  • 第一步是可选的,校验APK的签名,当前应用是不是谷歌市场下载的,如果不是从谷歌市场下载的那么支付不生效。如果你想要的校验APK来源就加上,不想校验也可以。

  • 第二步就是开始校验谷歌内购支付订单的状态,拿到本地长期保存的refresh_token 以及之前获取到的client_id 和 client_secret 就可以到哪授权的 access_token 。

  • 第三部就是拿到 access_token 以及 客户端传递的包名,产品id,支付凭证,调用校验接口,拿到订单的当前状态。
    然后就是根据订单的状态判断返回给客户端是否有效,让客户端执行消耗操作。
    如果您觉得有必要,也可以消耗之后再次调用接口校验,是否已购买,是否已消耗。

4.4、 创建Service Account的授权

其实之前的之前的 Web-OAuth 的方式来进行验证不是不行,但是步骤相对比较复杂,而更推荐的方式则是创建服务的方式来进行校验。
我们把视角拉回谷歌市场控制台,找到 Api Access 选项
在这里插入图片描述

其实我们在下面的访问权限就可以看到 Service Account 的选项。如果你已有 Service Account 就可以看到全部的关联的 Service Account 。如果你没有此服务,那么就可以点击创建服务去谷歌云创建。当我们到谷歌云里面点击创建 Service Account:
在这里插入图片描述

我们点击创建 Service Account 会走到创建服务的流程:
在这里插入图片描述

第一步随便写,关键是第二步:
在这里插入图片描述

选择角色为 Service Account Admin
在这里插入图片描述

第三步不填,直接提交:
在这里插入图片描述

你就能看到你创建的服务啦,接下来就是创建Key,Json的方式创建,然后下载到Json给到后台人员。
在这里插入图片描述

再下一步就回到谷歌商店控制台的 Api Access 看 Service Account 是否已经关联上了:
在这里插入图片描述

如果有这样的信息,说明关联上了,才是正确的流程,如果你创建了 Service Account,但是这里并没有展示,那么就肯定会错:

{  
  "code" : 401,  
  "errors" : [ {  
    "domain" : "androidpublisher",  
    "message" : "The current user has insufficient permissions to perform the requested operation.",  
    "reason" : "permissionDenied"  
  } ],  
  "message" : "The current user has insufficient permissions to perform the requested operation."  
}

之后正常显示了服务,说明你的服务才能访问到谷歌市场这一边,接下来就是点击授予访问权限。
重点是要把财务信息的两项勾选上,这样才能访问到应用内支付校验的相关权限,如图所示:
在这里插入图片描述

点击保存修改之后就完成了,由于我们关联账号的时候已经勾选了 Google Play Android Developer API 权限,我们现在直接就能用了。

后端的用法各平台的使用方式不同,但是都是很简单的,直接集成谷歌的API,然后总共就两步,第一步设置Config属性把这个 Service Account 生成的Json文件传入,第二步直接调用 GoogleAPI 内置的校验方法即可,都是API内置了的更方便。

当我们客户端把packageName ,prodectId,purchaseToken 三个字段传给后端,他们直接调用 API 就能直接校验,相比 Web-OAuth 的方式要更简单一些。
校验结果如下:
在这里插入图片描述

OK,两种方法 Web-OAuth 的授权方式,以及 Service Account 的授权方式,两种都可以达到效果,用哪种都可以。
至此谷歌内购全部流程已结束。

丢单问题处理

使用
_inAppPurchase.purchaseStream是用来监听消息队列的回调的,也就是所有订单的状态以及信息回调,in_app_purchase这个属性的文档中这么说到:

IMPORTANT! You must subscribe to this stream as soon as your app launches,
preferably before returning your main App Widget in main(). Otherwise you
will miss purchase updated made before this stream is subscribed to.
重要!你必须在应用程序启动后立即订阅此流,
最好在main()中返回主应用程序小部件之前。
否则你将错过订阅此流之前更新的购买。

也就是说当我们的App在第一次启动的时候可以订阅此流来完成补单的操作,但是如果用户是之前丢单了,然后把App又卸载了,再次下载打开App后并没有进行登录操作,那用户的登录信息都拿不到怎么进行补单操作呢?

补单解决方案

让后端出一个补单的接口,在补单时只需要传一个订单号即可,那App都删除了,之前的订单号客户端怎么获取呢?使用flutter_keychain来实现,flutter_keychain就是使用的iOS的钥匙串来实现的,当用户在苹果服务器下单时,在钥匙串中保存后端生成的订单号,然后再商品成功发货后删除钥匙串里面的订单号,完成一个完整的购买过程,再购买时任何一环出了问题钥匙串里面缓存的订单号都不会被清空,这样在App下一次启动时,在首页或者main函数中使用_inAppPurchase.purchaseStream 监听,在拿到flutter_keychain中保存的订单号完成补单过程。

注意点

1.在完成苹果服务器付款流程后通知到自己服务器接口也就是验单的接口返回的是成功或者不成功都要调用_inAppPurchase.completePurchase(purchaseDetails)这个方法,不然下次就掉不起苹果支付来了,当然肯定会在失败的判断里面写明白让用户自己去走苹果退款流程的文案(概率较小,但是也得考虑)

2.商品类型如果是非消耗品的话,在下单完之后一定写一个按钮供点击调用复原的方法,要是不复原的话每次下的订单,订单号都是一样的(需要注意下)。

参考:

Flutter插件:
pay 2.0.0 :https://pub.dev/packages/pay
in_app_purchase 3.2.0:https://pub.dev/packages/in_app_purchase

Google pay:
https://developers.google.com/pay/api/android/overview?hl=zh-cn
https://developer.android.com/google/play/billing

Apple pay:
https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay/setting_up_apple_pay
https://developer.apple.com/in-app-purchase/

教程:
https://juejin.cn/post/7290009513470623800
https://juejin.cn/post/7020651416276434958
https://blog.csdn.net/mumubumaopao/article/details/136112183
https://juejin.cn/post/7233310081809760317

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/598811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何为数据库中新建用户B复制用户A的表和视图权限?

故事背景&#xff1a; 公司使用的是SQL Server数据库&#xff0c;经常会碰到一种情况&#xff0c;需要为新入职的员工赋予同组内其他同事的权限。 常用方法: 1) 为同一组申请创建统一的Security Group(安全组)&#xff0c;为创建的组分配相关表和视图的访问权限。不管员工入职…

基于POSIX标准库的读者-写者问题的简单实现

文章目录 实验要求分析保证读写、写写互斥保证多个读者同时进行读操作 读者优先实例代码分析 写者优先示例代码分析 实验要求 创建一个控制台进程&#xff0c;此进程包含n个线程。用这n个线程来表示n个读者或写者。每个线程按相应测试数据文件的要求进行读写操作。用信号量机制…

FileLink跨网文件交换,推动企业高效协作|半导体行业解决方案

随着信息技术的迅猛发展&#xff0c;全球信息产业已经迎来了前所未有的繁荣与变革。在这场科技革命中&#xff0c;半导体作为信息产业的基础与核心&#xff0c;其重要性日益凸显&#xff0c;半导体的应用场景和市场需求将进一步扩大。 然而&#xff0c;在这一繁荣的背后&#x…

解决 SyntaxError: Unexpected token ‘.‘ 报错问题

这个报错一般是编译问题&#xff0c;浏览器的版本过低没通过代码 解决办法&#xff1a; 在package.json文件中加上这个 "browserslist": ["> 1%","last 2 versions","not dead","not ie < 6","Android > 4&…

源代码防泄露可以通过哪些方法实现?七种有效方法分享

在当今数字化时代&#xff0c;访问安全和数据安全成为企业面临的重要挑战。传统的边界防御已经无法满足日益复杂的内网办公环境&#xff0c;层出不穷的攻击手段已经让市场单一的防御手段黔驴技穷。当企业面临越来越复杂的网络威胁和数据泄密风险时&#xff0c;更需要一种综合的…

stable-diffusion-webui配置

源码地址 https://github.com/AUTOMATIC1111/stable-diffusion-webui.git报错Fresh install fail to load AttributeError: NoneType object has no attribute _id pydantic降级 pip uninstall pydantic pip install pydantic1.10.11记得要把clip-vit-large-patch14放在opena…

Java集合 总结篇(全)

Java集合 集合底层框架总结 List 代表的有序&#xff0c;可重复的集合。 ArrayList -- 数组 -- 把他想象成C中的Vector就可以&#xff0c;当数组空间不够的时候&#xff0c;会自动扩容。 -- 线程不安全 LinkedList -- 双向链表 -- 可以将他理解成一个链表&#xff0c;不支持…

C语言猜数字游戏

用C语言实现猜数字游戏&#xff0c;电脑随机给出一个范围内的数字&#xff0c;用户在终端输入数字&#xff0c;去猜大小&#xff1b;对比数字&#xff0c;电脑给出提示偏大还是偏小&#xff1b;不断循环&#xff0c;直到正确 #include <stdio.h> #include <time.h>…

【系统架构师】-选择题(十一)

1、紧耦合多机系统一般通过&#xff08;共享内存&#xff09;实现多机间的通信。对称多处理器结构&#xff08;SMP&#xff09;属于&#xff08; 紧耦合&#xff09;系统。 松耦合多机系统又称间接耦合系统,—般是通过通道或通信线路实现计算机间的互连。 2、采用微内核的OS结构…

从互联网医院源码到搭建:开发视频问诊小程序的技术解析

如今&#xff0c;视频问诊小程序作为医疗服务的一种新形式&#xff0c;正逐渐受到人们的关注和青睐。今天&#xff0c;小编将为您详解视频问诊小程序的开发流程。 一、背景介绍 互联网医院源码是视频问诊小程序开发的基础&#xff0c;它提供了一套完整的医疗服务系统框架&…

【vue-echarts】 报错问题解决 “Error: Component series.pie not exists. Load it first.“

目录 问题描述解决【解决1】【解决2】 问题描述 使用 vue-echarts 时导入的文件 import VChart from vue-echarts/components/ECharts import echarts/lib/chart/line import echarts/lib/chart/bar import echarts/lib/chart/pie import echarts/lib/component/legend impor…

MySQL 报错: “Host ‘xxx‘ is not allowed to connect to this MySQL server“

MySQL 报错 “Host ‘xxx’ is not allowed to connect to this MySQL server” 通常是因为数据库服务器上的权限设置不允许来自特定主机&#xff08;‘xxx’&#xff09;的连接。解决这个问题通常涉及修改 MySQL 的访问控制设置。 以下是一些可能的解决步骤&#xff1a; 使用…

高效工作之:开源工具kettle实战

在运营商数据处理领域&#xff0c;Oracle存储过程一直是数据处理的核心工具&#xff0c;但随着技术的发展&#xff0c;寻找替代方案变得迫切。Kettle&#xff0c;作为Oracle存储过程的替代品&#xff0c;以其强大的功能和易用性&#xff0c;正逐渐受到运营商的青睐。本文将介绍…

C++基础——深拷贝和浅拷贝

C中类的拷贝有两种&#xff1a;深拷贝&#xff0c;浅拷贝&#xff1a;当出现类的等号赋值时&#xff0c;即会调用拷贝函数 一、概念 浅拷贝&#xff1a;同一类型的对象之间可以赋值&#xff0c;使得两个对象的成员变量的值相同&#xff0c;两个对象仍然是独立的两个对象&#…

【全网首发】Typecho文章采集器火车头插件去授权版

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 目前市面上基本没有typecho火车头采集器 而分享的这一款采集器&#xff0c;牛的一批 内置使用方法与教程&#xff01; 二、效果展示 1.部分代码 代码如下&#xff08;示例&#…

嘎嘎好用的虚拟键盘第二弹之中文输入法

之前还在为不用研究输入中文而暗自窃喜 这不新需求就来了&#xff08;新需求不会迟到 它只是在路上飞一会儿&#xff09; 找到了个博主分享的代码 是好使的 前端-xyq 已经和原作者申请转载了 感谢~~ 原作者地址&#xff1a;https://www.cnblogs.com/linjiangxian/p/16223681.h…

Amazon Q Business现已正式上市!利用生成式人工智能协助提高员工生产力

在 2023 年度 AWS re:Invent 大会上&#xff0c;我们预览了 Amazon Q Business&#xff0c;这是一款基于生成式人工智能的助手&#xff0c;可以根据企业系统中的数据和信息回答问题、提供摘要、生成内容额安全地完成任务。 借助 Amazon Q Business&#xff0c;您可以部署安全、…

Java多线程编程之synchronizaed和锁分类

并发编程第三周 1 锁的分类 1.1 可重入锁&#xff0c;不可重入锁 Java提供的synchronized&#xff0c;ReentrantLock,ReentrantReadWriteLock都是可重入锁 可重入&#xff1a;当前线程获取到A锁&#xff0c;在获取之后尝试再次获取A锁是可以直接拿到的。 不可重入:当前线程…

python使用mongo操作

目前有个需求&#xff0c;就是把所有sql转为mongo管道查询 知识点 在 MongoDB 中&#xff0c;allowDiskUse 选项应该作为聚合命令的一个选项&#xff0c;而不是聚合管道的一个阶段。allowDiskUse 选项用于允许聚合操作使用磁盘空间来临时存储数据&#xff08;当聚合操作的数据…

[leetcode] 67. 二进制求和

文章目录 题目描述解题方法模拟java代码复杂度分析 相似题目 题目描述 给你两个二进制字符串 a 和 b &#xff0c;以二进制字符串的形式返回它们的和。 示例 1&#xff1a; 输入:a "11", b "1" 输出&#xff1a;"100"示例 2&#xff1a; 输…
最新文章