【Flutter】App集成谷歌内购(GooglePay)的全过程(全程详细图文加代码)

3,770 阅读13分钟

Flutter集成谷歌内购与使用的全过程

我正在参加「掘金·启航计划」

前言

注意:本文包含大量的图片与代码,注意流量使用。

在之前的文章中,我们了解到 Flutter 的页面开发,以及 Flutter 的架构搭建,完成对应的页面与逻辑是没什么问题了。

但是开发一个 App 又不是只有页面的基本逻辑,如果涉及到一些第三方的对接就显得比较麻烦,比如推送,支付,内购,IM 等等。

而如果第三方没有提供到 Flutter 的插件,那么我们就需要通过 Channel 的方式自己去实现 Android 与 iOS 的具体实现。

还好,Flutter 推出这么些年,基本上常用的一些插件都已经有支持,就算官方没出,也有民间大神出了类似的插件。

而本文所说的就是相对比较复杂的第三方集成,应用商城内购,大部分是讲谷歌内购,其实苹果内购比谷歌内购要更容易一下,兼容一下同样的代码就能完成相关的流程。

以谷歌内购为例,我们需要做的总共为三步,每一步我会给出详细的相关设置对应的图片或代码。

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

注意:由于谷歌市场版本老在变化,本文的谷歌市场以 2023-05-02 时间为准,如果后期谷歌市场与谷歌云的相关网页布局 或 Tab 发生变化,大家可以参考使用。

好了,大致的步骤了解了之后,我们就开始一步步的走相关流程。

300.png

一、谷歌市场配置商品

按道理,我们只需要在谷歌市场中,找到指定的应用,然后在内购的 Tab 中直接添加商品即可。但是并没有这么简单,会提示报错,没有设置付款账号。

1.1 定价模板

所以我们要切换到整个账号的设置页面,而不是指定应用的选项。

image.png

找到付款概况之后,如果没有付款账号,我们填写一些信息,姓名,邮箱,账号,等等信息,创建完成之后我们就可以设置定价的模板。

如果能创建模板说明你付款账号没问题,定价模板是非必须的,可有可无,但是定义了模板之后会更加方便,到时候创建商品可以直接关联模板,账号下的每一个子应用的内购商品都能关联对应的模板,有一个统一的定价。

如何创建定价模板如下:

image.png

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

image.png

创建完成之后,我们就能看到对应的定价模板如下图所示:

image.png

1.2 应用上架

当我们的一些定价模板定义完成之后,我们就可以设置应用的内购了,点击应用商品内购的 Tab ,结果是不能创建,因为你虽然创建了子应用,但是你没有上传APK包,并没有应用所以没有对应的应用内商品。

所以在此之前我们还需要做一些配置。我们需要创建一个内部测试渠道,然后配置对应的开发者或测试人员。

哎,对对对,我知道是很麻烦,很SB,但谁叫谷歌就是这么定的流程呢,没有办法,按流程走把。

首先需要创建一个测试轨道:比如我选择的是内部测试:

image.png

发布一个 APK / AAB 文件到测试渠道,然后 选择/创建 测试人员。

image.png

如果没有的话,创建一个测试者,输入对应的邮箱即可。

这样就能添加测试人员了,同时你还可以在底部邀请链接去邀请测试者,激活测试规则等。

当一切都配置完成之后(会有一个进度条的,当完成一步会中划线标记的),我们到发布中选择审核以及发布,谷歌审核很快,一切正常2个小时内就能通过,然后我们打开谷歌应用市场搜索我们的应用就能找到内部测试的渠道应用:

image.png

点击下载之后,我们安装的就是我们之前创建的内部测试渠道应用啦。

1.3 创建应用内购商品

应用虽然是上线了,但是我们还没创建内购商品呢,此时我们再点击应用内购的 Tab 就能创建对应的商品了。

image.png

商品其实是分订阅类型与内购商品,内购商品又分消耗型与非消耗型,由于我们的商品并不复杂,是输入消耗型的,所以我的代码都是以消耗性为例。

如何区别各种类型?举个栗子:

  1. 订阅类型:爱奇艺的VIP月卡。
  2. 非消耗性:消消乐免费玩前10关卡,付费解锁后面的关卡内容。
  3. 消耗性:欢乐斗地主买100个欢乐豆。

我们其实就是最简单的消耗性,花钱买虚拟币,可重复购买的那种。

下面开始创建商品,这一点反倒是蛮简单:

image.png

注意的是,创建应用id的时候,最好是包名加上商品id。避免冲突

com.xxgroup.whatsapp.coin10

随后定义对应的标题与描述,以及商品对应的价格,可以使用价格模板,也可以直接写。

image.png

创建完成之后别急,需要激活之后才能生效:

image.png

此时列表上就有了,这样才是生效状态。

image.png

此时就能买了吗?可以了,但是测试账号并没有关联信用卡,也不方便,我们设置一下测试账号的测试购买授权:

image.png

下面的授权方式改为:LICENSED

image.png

好了到此,我们的谷歌市场配置就完成了。

二、集成支付插件兼容Android与iOS支付

接下来我们就在 Flutter 中使用插件集成应用内支付功能。

  in_app_purchase: 3.1.5

文档地址:【传送门】

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

贴一下我的代码:

首先是生命周期的回调,页面创建完成初始化插件,并设置监听,当页面关闭的时间销毁资源:

  late StreamSubscription<List<PurchaseDetails>> _subscription;
  late InAppPurchase _inAppPurchase;
  List<ProductDetails>? _products; //内购的商品对象集合

  @override
  void onReady() {
    fetchCoinList();

    // 初始化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();
      SmartDialog.showToast("购买失败了");
    });

    //加载全部的商品
    loadProducts();
  }

  @override
  void onClose() {
    refreshController.dispose();
    datas.clear();
    if (Device.isIOS) {
      final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
          _inAppPurchase.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
      iosPlatformAddition.setDelegate(null);
    }
    _subscription.cancel();
  }

加载全部的商品,这里真实开发环境是由后端接口返回的,我这里作为测试就写死了:

  /// 加载全部的商品
  void loadProducts() async {
    final bool available = await _inAppPurchase.isAvailable();
    if (!available) {
      SmartDialog.showToast("无法连接到商店");
      return;
    }

    //开始购买
    SmartDialog.showToast("连接成功-开始查询全部商品");
    const Set<String> _kIds = <String>{
      'com.hongyegroup.whatsapp.android.coin100',
      'com.hongyegroup.whatsapp.android.coin500',
      'com.hongyegroup.whatsapp.android.coin1000',
      'com.hongyegroup.whatsapp.android.coin3000',
    };
    final ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(_kIds);
    if (response.notFoundIDs.isNotEmpty) {
      SmartDialog.showToast("无法找到指定的商品");
      return;
    }

    // 处理查询到的商品列表
    List<ProductDetails> products = response.productDetails;
    if (products.isNotEmpty) {
      //赋值内购商品集合
      _products = products;
    }

    SmartDialog.showToast("全部商品加载完成了,可以启动购买了,总共商品数量为:${products.length}");

    //先恢复可重复购买
    await _inAppPurchase.restorePurchases();
  }

查询到商品之后就可以在 ListView 显示商品的属性,如过 UI 设计的列表样式比较复杂,当我们谷歌内购设置的标题与详情满足不了设计的需求,可以由我们自己的后台返回对应接口,当商品加载成功之后的商品 id 去匹配对应后台的列表数据并展示出来。

当我们点击对应的 Item 就可以拿到对应的商品ID,执行购买的逻辑:

  // 调用此函数以启动购买过程
  void startPurchase(String productId) async {
    if (_products != null && _products!.isNotEmpty) {
      SmartDialog.showToast("准备开始启动购买流程");
      try {
        ProductDetails productDetails = _getProduct(productId);

        Log.d("一切正常,开始购买,信息如下: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();
        Log.e("购买失败了");
      }
    } else {
      SmartDialog.showToast("当前没有商品无法调用购买逻辑");
    }
  }

   // 根据产品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.error) {
        // 购买失败
        _handleError(purchase.error);
      } else if (purchase.status == PurchaseStatus.purchased || purchase.status == PurchaseStatus.restored) {
        //完成购买, 到服务器验证
        if (Device.isAndroid) {
          var googleDetail = purchase as GooglePlayPurchaseDetails;
          print(purchase);
          loadAndroidGetPayInfo(googleDetail);
        } else if (Device.isIOS) {
          var appstoreDetail = purchase as AppStorePurchaseDetails;
          print(purchase);
          loadAppleGetPayInfo(appstoreDetail);
        }
      }
    }
  }

  /// 购买失败
  void _handleError(IAPError? iapError) {
    SmartDialog.showToast("购买失败啦:${iapError?.code} message${iapError?.message}");
  }

  /// 等待支付
  void _handlePending() {
    SmartDialog.showToast("等待支付的逻辑");
  }

  /// Android支付成功的校验
  void loadAndroidGetPayInfo(GooglePlayPurchaseDetails googleDetail) async {
    final originalJson = googleDetail.billingClientPurchase.originalJson;

    Log.d("originalJson:$originalJson");

    if (await coinRepositroy.checkGooglePaySuccess(originalJson)) {
      //校验成功之后执行消耗
      await _inAppPurchase.completePurchase(googleDetail);
    }
  }

  /// Apple支付成功的校验
  void loadAppleGetPayInfo(AppStorePurchaseDetails appstoreDetail) {
    if (await coinRepositroy.checkApplyPaySuccess(appstoreDetail)) {
      //校验成功之后执行消耗
      await _inAppPurchase.completePurchase(appstoreDetail);
    }
  }

到这一步,我们已经和测试账号管理了,所以我们直接运行 Debug 包一样的可以测试支付,并且我们授权测试支付,所以不需要绑定银行卡直接用测试支付卡就能完成支付流程。

截图:

image.png

image.png

当我们支付成功之后就可以执行消耗操作。

三、服务器校验相关流程

我并没有把代码做区分,上面的代码就已经包含了支付成功之后的后端校验逻辑。

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

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

谷歌结算文档:【传送门】

谷歌支付校验AI:【传送门】

如果我们直接在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"
      }
    ]
  }
}

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

3.1 Google Cloud关联

首先需要配置 Google Cloud 并且配置相关的账号,对应指定的应用。

点击项目的 API Access 中

image.png

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

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

网上很多教程教你去开通 Google Play Developer API 权限,其实现在是没必要了,因为我们关联 Google Cloud 账号之后,默认就已经开通了。

image.png

所以我们不需要再次去授权了。

image.png

如果觉得不保险,也能在里面搜索 Billing ,然后启动相关的支付服务权限,反正我是开了,但我觉得没必要开这些,如果不行的你开启这些服务也无所谓。

3.2 创建 web-OAuth 授权

网上很多教程比较老,可能要你手动的创建web授权,其实当我们在谷歌市场的后台关联谷歌云的时候,就已经帮我们初始化了很多配置,已经都有了。

我们再谷歌云后台,在APIs & auth 项中找到 Credentials,直接查看即可:

image.png

我们点击 Web 授权进去配置相关配置。

主要是配置左侧的上下两个 URI 地址,上面的配置后台域名:

image.png

下面的是固定写法,callback的地址一定是可用域名 + /oauth2callback。

image.png

创建完成之后,记得记录你的三个重要字段,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 就是空了,不会返回了。

3.3 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,支付凭证,调用校验接口,拿到订单的当前状态。

然后就是根据订单的状态判断返回给客户端是否有效,让客户端执行消耗操作。

如果您觉得有必要,也可以消耗之后再次调用接口校验,是否已购买,是否已消耗。

3.3 创建Service Account的授权

其实之前的之前的 Web-OAuth 的方式来进行验证不是不行,但是步骤相对比较复杂,而更推荐的方式则是创建服务的方式来进行校验。

我们把视角拉回谷歌市场控制台,找到 Api Access 选项 (老熟人了)

image.png

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

image.png

我们点击创建 Service Account 会走到创建服务的流程:

image.png

第一步随便写,关键是第二步:

image.png

选择角色为 Service Account Admin

image.png

第三步不填,直接提交:

image.png

你就能看到你创建的服务啦,接下来就是创建Key,Json的方式创建,然后下载到Json给到后台人员。

image.png

image.png

再下一步就回到谷歌商店控制台的 Api Access 看 Service Account 是否已经关联上了:

image.png

如果有这样的信息,说明关联上了,才是正确的流程,如果你创建了 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."  
}

之后正常显示了服务,说明你的服务才能访问到谷歌市场这一边,接下来就是点击授予访问权限。

重点是要把财务信息的两项勾选上,这样才能访问到应用内支付校验的相关权限,如图所示:

image.png

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

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

当我们客户端把packageName ,prodectId,purchaseToken 三个字段传给后端,他们直接调用 API 就能直接校验,相比 Web-OAuth 的方式要更简单一些。

校验结果如下:

@3A0VN4P0@79~8E0GI5Q62.png

OK,两种方法 Web-OAuth 的授权方式,以及 Service Account 的授权方式,两种都可以达到效果。就看大家的喜欢哪一种啦!

那么到处整体的谷歌内购全部流程就结束了,大家觉得还算详细吗?

后记

其他的第三方插件我觉得 Flutter 都蛮简单的,比如极光推送,感觉比 Android 版本的还要简单,一些配置与代码都封装好了,开箱即用很方便。

对于内购的插件 in_app_purchase 其实内部在 Android 平台也是用的 Google Billing ,只是封装之后使用起来也很简单。感觉比原生都好用 😄😄

主要是内购的配置,谷歌的配置,应用配置,商品的配置,谷歌云配置,各种授权配置。只要其中一步卡住了就不能行,感觉真的是很复杂,网上的一些参考资源很多都是过时的,所以才有了我一步步的踩坑的分享,如果有需要大家可以收藏一波,收藏等于学会,万一哪天踩坑了可以参考参考嘛。

关于内购,不知道大家有没有遇到内购的一些坑呢,又是如何解决的呢?欢迎大家一起交流一下哦。

那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。