如何结合Firebase身份认证使用支付中心

如已使用Firebase在应用程序中实现用户身份认证,可在Firebase侧生成一个支付令牌,然后将其传入应用程序的客户端侧来打开支付UI。

如使用此集成方案,需独立实现决定用户国家/地区和支付所使用的货币的逻辑。

集成过程:

  1. 创建项目
  1. 注册发布商帐户并新建一个项目。后续步骤需要用到所创建项目的ID。

  1. 设置目录
    • 在艾克索拉侧创建一个商品目录。您可以手动添加商品或从Google Play或PlayFab导入。
    • 使用Store库实现在应用程序客户端侧获取和显示目录。

  1. 设置商品购买
    • 通过Firebase云函数使用用户和商品数据在应用程序客户端侧创建一个订单。
    • 使用Payments库实现在应用程序客户端侧打开支付UI。

  1. 设置订单状态跟踪

注意

要完成集成并开始接收真实付款,您需要签署与艾克索拉的许可协议。

您可以在集成的任意阶段签署协议,但请知晓审核过程最长可能需要3个工作日。

请使用样本Web应用程序作为示例来实现Firebase身份认证和支付中心的结合使用。样本Web应用程序的源代码在GitHub上提供。

创建项目

发布商帐户是配置艾克索拉功能以及对分析和交易进行操作的主要工具。

注册时指定的公司及应用程序信息将用于创建与艾克索拉许可协议的草稿以及生成合适解决方案的推荐。后续您可以更改这些数据,但在注册时提供正确的信息可以加快签署许可协议的速度。

要创建项目:

  1. 注册发布商帐户
注:
密码生成规则

发布商帐户的密码可以包含英文字母、数字和特殊字符,且必须至少包含:

  • 8个字符
  • 一个数字
  • 一个大写字母
  • 一个小写字母

为确保密码安全性,建议您:

  • 每90天至少更改一次密码
  • 使用与帐户中过去4个密码不同的新密码
  • 使用与其他地方的密码不同的特殊密码
  • 勿将密码保存在可以轻易访问的地方
  • 使用密码管理器来保存您的密码

发布商帐户使用双因素认证,每次认证尝试都会发送验证码。

  1. 提供以下信息创建您的帐户:
    1. 您的名和姓。
    2. 您在公司中的职位。
    3. 公司名称。如果以个人身份注册帐户,请输入您的全名。
    4. 关于您和您的游戏或产品的详细信息链接(可选)。
    5. 您的国家/地区。
  1. 单击下一步
  1. 要自动创建您的首个项目,请指定关于项目的主要信息:
    1. 选择项目类型:游戏游戏平台其他
    2. 用英文输入项目名称。
    3. 选择一个或多个发布平台。
    4. 选择您正在使用或计划使用的盈利方式。
    5. 选择开发阶段。
    6. 如果您的项目类型是游戏,请选择其类型和游戏引擎。
    7. 添加您的游戏或产品链接(可选)。
  1. 单击完成

项目创建后,您将被重定向到公司页面。创建的项目会显示在侧边栏中:

  • 单击其名称开始集成艾克索拉产品和解决方案。
  • 前往项目设置部分添加其他语言和本地化项目名称(可选)。

如果有多个游戏或产品,请为每个创建单独的项目。为此,在公司页面的侧边栏中,单击创建项目并指定必要信息。

集成过程中需提供项目ID,可在发布商帐户中项目名称的旁边找到。

设置目录

在发布商帐户中创建商品

注意

您需要在艾克索拉侧创建一个目录。您可以手动添加商品从App Store、Google Play或PlayFab导入商品。从Google Play导入时,一次最多可以导入100个商品。

以下说明提供的是设置虚拟物品的基本步骤。您可以在后来向目录添加其他商品(虚拟货币、捆绑包、游戏密钥等)、创建商品组、设置促销活动和区域价格等。

要向目录添加具有基本设置的虚拟物品:

  1. 在发布商帐户中打开您的项目,前往商品目录 > 虚拟物品部分。
  2. 在下拉菜单中选择创建物品

  1. 在以下字段中完成物品的基本设置:
    • 图片(可选)
    • SKU(物品唯一ID)
    • 物品名称
    • 描述(可选)

  1. 指定物品价格:
    1. 真实货币定价开关设置为
    2. 默认货币字段,更改货币(可选)并指定物品价格。
    3. 如更改了默认货币字段中的货币,请在真实货币定价字段中选择同样的货币。

注:
为确保获取目录的API调用正常工作,请确保所有物品的默认货币和指定价格的货币列表一致。

  1. 将物品状态更改为可用

  1. 单击创建物品

在应用程序客户端侧显示目录

  1. Store库添加到您的项目。方法是打开build.gradle,然后在依赖关系部分添加以下行:

Copy
Full screen
Small screen
    1implementation("com.xsolla.android:store:latest.release")
    

    1. 在应用程序的客户端侧,添加显示产品目录的UI。
    2. 实现向艾克索拉服务器请求商品目录。

    注:
    示例中使用了XStore.getVirtualItems方法来获取虚拟物品列表。您也可以使用其他Store库方法来获取目录商品的信息。

    示例(样本web应用程序的StoreActivity类):

    Copy
    Full screen
    Small screen
     1package com.xsolla.androidsample
     2
     3import androidx.appcompat.app.AppCompatActivity
     4import android.os.Bundle
     5import android.widget.Toast
     6import androidx.recyclerview.widget.LinearLayoutManager
     7import androidx.recyclerview.widget.RecyclerView
     8import com.xsolla.android.store.XStore
     9import com.xsolla.android.store.callbacks.GetVirtualItemsCallback
    10import com.xsolla.android.store.entity.response.items.VirtualItemsResponse
    11import com.xsolla.androidsample.adapter.BuyItemAdapter
    12
    13class StoreActivity : AppCompatActivity() {
    14
    15    private lateinit var itemsView: RecyclerView
    16
    17    override fun onCreate(savedInstanceState: Bundle?) {
    18        super.onCreate(savedInstanceState)
    19        setContentView(R.layout.activity_store)
    20
    21        XStore.init(<projectId>)
    22
    23        initUI()
    24        loadVirtualItems()
    25    }
    26
    27    private fun initUI() {
    28        itemsView = findViewById(R.id.buy_recycler_view)
    29        itemsView.layoutManager = LinearLayoutManager(this)
    30    }
    31
    32    private fun loadVirtualItems() {
    33        val parentActivity = this
    34        XStore.getVirtualItems(object : GetVirtualItemsCallback {
    35            override fun onSuccess(response: VirtualItemsResponse) {
    36                itemsView.adapter = BuyItemAdapter(parentActivity, response.items.filter { item -> item.virtualPrices.isEmpty() && !item.isFree })
    37            }
    38
    39            override fun onError(throwable: Throwable?, errorMessage: String?) {
    40                showNotificationMessage(errorMessage ?: throwable?.javaClass?.name ?: "Error")
    41            }
    42        })
    43    }
    44
    45    private fun showNotificationMessage(message: String) {
    46        Toast.makeText(
    47            baseContext,
    48            message,
    49            Toast.LENGTH_SHORT,
    50        ).show()
    51    }
    52}
    

    在脚本的XStore.init()初始化区块,指定可在发布商帐户项目名称旁边找到的项目ID。

    设置商品购买

    使用云函数创建订单

    要使用用户和商品数据在艾克索拉侧创建订单,请添加使用为购买创建支付令牌API调用的云函数至项目。该调用将返回一个支付令牌,用于打开支付UI并进行购买。

    限制:

    • 需在请求支付令牌时传入用户国家/地区或用户IP地址。
    • 如未在令牌中传入货币,则货币由国家/地区决定。
    • 如在令牌中传入了货币,则用户用该货币进行支付。

    注:
    您需要先创建并初始化一个Firebase项目,然后使用Firebase激活用户认证。关于上述步骤的详细信息,请参阅以下Firebase说明:

    要将云函数添加至项目:

    1. 安装Firebase CLI(命令行界面)。方法是运行以下CLI命令:

    Copy
    Full screen
    Small screen
      1npm install -g firebase-tools
      

      1. 要将您的项目关联至Firebase项目,请运行以下CLI命令来初始化Firebase项目:

      Copy
      Full screen
      Small screen
        1firebase init functions
        

        1. 按照安装程序的说明来配置设置:
          1. 选择一个现有代码库。
          2. 指定JavaScript作为创建云函数的语言。
          3. 安装依赖项。

        1. 打开functions/index.js并进行修改:

        Copy
        Full screen
        Small screen
         1// The Cloud Functions for Firebase SDK to create Cloud Functions and triggers.
         2const functions = require('firebase-functions/v1');
         3
         4const projectId = <projectId>;
         5const apiKey = <apiKey>;
         6
         7exports.getXsollaPaymentToken = functions.https.onRequest((req, res) => {
         8
         9  const requestBody = req.body;
        10    if (!requestBody) {
        11      res.status(400).send('Request body is missing');
        12      return;
        13    }
        14
        15  const userId = requestBody.data.uid;
        16  const email = requestBody.data.email;
        17  const sku = requestBody.data.sku;
        18  const returnUrl = requestBody.data.returnUrl;
        19
        20  const payload = {
        21    user: {
        22      id: {value: userId},
        23      name: {
        24        value: email
        25      },
        26      email: {
        27        value: email
        28      },
        29      country: {
        30        value: 'US',
        31        allow_modify: false
        32      }
        33    },
        34    purchase: {
        35      items: [
        36        {
        37          sku: sku,
        38          quantity: 1
        39        }
        40      ]
        41    },
        42    sandbox: true,
        43    settings: {
        44      language: 'en',
        45      currency: 'USD',
        46      return_url: returnUrl,
        47      ui: {
        48        theme: '63295aab2e47fab76f7708e3'
        49      }
        50    }
        51  }
        52
        53  let url = `https://store.xsolla.com/api/v3/project/${projectId.toString()}/admin/payment/token`;
        54
        55  fetch(
        56    url,
        57    {
        58      method: "POST",
        59      headers: {
        60        'Content-Type': 'application/json',
        61        Authorization: `Basic ${btoa(`${projectId}:${apiKey}`)}`
        62      },
        63      body: JSON.stringify(payload)
        64    },
        65  )
        66  .then(xsollaRes => {
        67    // Handle the response data
        68    if (xsollaRes.ok) {
        69      return xsollaRes.json();
        70    } else {
        71      throw new Error(`HTTP request failed with status ${xsollaRes.status} and statusText: ${xsollaRes.statusText}`)
        72    }
        73  })
        74  .then(data => {
        75    res.send(JSON.stringify(data));
        76  })
        77  .catch(error => {
        78    res.send(`Error = ${error}`);
        79  });
        80});
        81
        82exports.webhookFakeResponse = functions.https.onRequest((request, response) => {
        83  response.status(200).send()
        84})
        

        1. 在脚本中,指定变量的值:
          • projectId — 可在发布商帐户中项目名称旁边找到的项目ID。

        1. 要使用模拟器测试云函数,请运行CLI命令:

        Copy
        Full screen
        Small screen
          1firebase emulators:start
          

          1. 运行云函数后,您可以在应用程序客户端侧调用以下方法:
            • getXsollaPaymentToken — 返回用于打开支付界面的支付令牌。
            • webhookFakeResponse — 在对支付Webhook的响应中发送HTTP代码200。该方法不包含购买验证逻辑:请仅在测试中使用。对于Webhook的完整列表以及常规使用信息,请参阅Webhook文档

          1. 要在本地调用方法,请使用https://localhost:5001/{firebase-project-id}/us-central1/getXsollaPaymentTokenhttps://localhost:5001/{firebase-project-id}/us-central1/webhookFakeResponse URL,其中{firebase-project-id}是Firebase项目ID(Firebase控制台 > Project Settings > Project ID)。

          1. 要在生产中部署云函数,请运行CLI命令:

          Copy
          Full screen
          Small screen
            1firebase deploy --only functions
            

            1. 在生产中部署后,即可通过https://us-central1-{firebase-project-id}.cloudfunctions.net/getXsollaPaymentTokenhttps://us-central1-{firebase-project-id}.cloudfunctions.net/webhookFakeResponse URL调用方法,其中{firebase-project-id}是Firebase项目ID(Firebase控制台 > Project Settings > Project ID)。关于在生产环境中运行功能的详细信息,请参阅Firebase文档

            设置支付UI的启动

            1. Payments库添加到您的项目。方法是打开build.gradle,然后在依赖关系部分添加以下行:

            Copy
            Full screen
            Small screen
              1implementation("com.xsolla.android:payments:latest.release")
              

              1. 打开AndroidManifest.xml,然后添加网络访问的权限:

              Copy
              Full screen
              Small screen
              1<uses-permission android:name="android.permission.INTERNET" />
              

              1. 添加订单创建逻辑(调用云函数的XStore.getXsollaPaymentToken方法)并使用收到的支付令牌(XPayments.createIntentBuilder()类打开支付UI)。

              1. 要调用getXsollaPaymentToken方法,请提供以下URL之一,其中{firebase-project-id}是Firebase项目ID(Firebase控制台 > Project Settings > Project ID):
                • 对于本地访问 — https://localhost:5001/{firebase-project-id}/us-central1/getXsollaPaymentToken
                • 对于生产环境中的访问 — https://us-central1-{firebase-project-id}.cloudfunctions.net/getXsollaPaymentToken

              示例(样本web应用程序的BuyItemAdapter类):

              Copy
              Full screen
              Small screen
                1package com.xsolla.androidsample.adapter
                2
                3import android.R.attr.duration
                4import android.os.Handler
                5import android.os.Looper
                6import android.view.LayoutInflater
                7import android.view.ViewGroup
                8import android.widget.Toast
                9import androidx.recyclerview.widget.RecyclerView
               10import com.bumptech.glide.Glide
               11import com.xsolla.android.payments.XPayments
               12import com.xsolla.android.payments.data.AccessToken
               13import com.xsolla.android.store.entity.response.items.VirtualItemsResponse
               14import com.xsolla.androidsample.R
               15import com.xsolla.androidsample.StoreActivity
               16import org.json.JSONObject
               17import java.io.BufferedReader
               18import java.io.BufferedWriter
               19import java.io.OutputStream
               20import java.io.OutputStreamWriter
               21import java.net.HttpURLConnection
               22import java.net.URL
               23
               24
               25class BuyItemAdapter(private val parentActivity: StoreActivity, private val items: List<VirtualItemsResponse.Item>) :
               26    RecyclerView.Adapter<BuyItemViewHolder>() {
               27    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BuyItemViewHolder {
               28        return BuyItemViewHolder( LayoutInflater.from(parent.context)
               29            .inflate(R.layout.buy_item_sample, parent, false))
               30    }
               31
               32    override fun onBindViewHolder(holder: BuyItemViewHolder, position: Int) {
               33        val item = items[position]
               34        Glide.with(holder.view).load(item.imageUrl).into(holder.itemImage)
               35        holder.itemName.text = item.name
               36        holder.itemDescription.text = item.description
               37        var priceText: String
               38        if(item.virtualPrices.isNotEmpty()) {
               39            priceText = "${item.virtualPrices[0].getAmountRaw()} ${item.virtualPrices[0].name}"
               40        } else {
               41            priceText = "${item.price?.getAmountRaw()} ${item.price?.currency.toString()}"
               42        }
               43
               44        holder.itemPrice.text = priceText
               45
               46        holder.itemButton.setOnClickListener {
               47            Thread {
               48                purchase(item.sku!!)
               49            }.start()
               50        }
               51    }
               52
               53    private fun purchase(sku: String) {
               54
               55        val uid = parentActivity.intent.getStringExtra("uid")
               56        val email = parentActivity.intent.getStringExtra("email")
               57
               58        val jsonBody = JSONObject()
               59        jsonBody.put("data", JSONObject().apply {
               60            put("uid", uid)
               61            put("email", email)
               62            put("sku", sku)
               63            put("returnUrl", "app://xpayment.${parentActivity.packageName}")
               64        })
               65
               66        val connection = URL(https://localhost:5001/{firebase-project-id}/us-central1/getXsollaPaymentToken).openConnection() as HttpURLConnection
               67        connection.requestMethod = "POST"
               68        connection.setRequestProperty("Content-Type", "application/json")
               69        connection.doOutput = true
               70
               71        val outputStream: OutputStream = connection.outputStream
               72        val writer = BufferedWriter(OutputStreamWriter(outputStream))
               73        writer.write(jsonBody.toString())
               74        writer.flush()
               75        writer.close()
               76
               77        val responseCode = connection.responseCode
               78
               79        if (responseCode == HttpURLConnection.HTTP_OK) {
               80            val response = connection.inputStream.bufferedReader().use(BufferedReader::readText)
               81            connection.disconnect()
               82
               83            val jsonObject = JSONObject(response)
               84            val token = jsonObject.getString("token")
               85            val orderId = jsonObject.getString("order_id")
               86
               87            val intent = XPayments.createIntentBuilder(parentActivity)
               88                .accessToken(AccessToken(token))
               89                .isSandbox(true)
               90                .build()
               91            parentActivity.startActivityForResult(intent, 1)
               92        } else {
               93            Handler(Looper.getMainLooper()).post {
               94                showNotificationMessage("HTTP request failed with error: $responseCode")
               95            }
               96        }
               97    }
               98
               99    override fun getItemCount() = items.size
              100
              101    private fun showNotificationMessage(message: String) {
              102        Toast.makeText(
              103            parentActivity,
              104            message,
              105            Toast.LENGTH_SHORT,
              106        ).show()
              107    }
              108}
              

              1. 添加onActivityResult()方法以处理支付结果。

              示例(样本web应用程序的StoreActivity类):

              Copy
              Full screen
              Small screen
               1override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
               2        super.onActivityResult(requestCode, resultCode, data)
               3        if (requestCode == 1) {
               4            val (status, _) = XPayments.Result.fromResultIntent(data)
               5            when (status) {
               6                XPayments.Status.COMPLETED -> showNotificationMessage("Payment completed")
               7                XPayments.Status.CANCELLED -> showNotificationMessage("Payment canceled")
               8                XPayments.Status.UNKNOWN -> showNotificationMessage("Payment error")
               9            }
              10        }
              11    }
              

              设置订单状态跟踪

              您需要跟踪项目状态,以确保支付成功并向用户发放商品。

              获取客户端侧订单状态

              要订阅应用程序客户端侧状态更改,请调用XStore.getOrderStatus方法,并向方法传入以下参数:

              • listenerOrderStatusListener类型的侦听器对象。
              • orderId — 从购物车下单、一键购买或用虚拟货币购买收到的订单ID。

              关于方法的详细使用信息,请参阅跟踪订单状态部分。

              在服务器侧获取订单状态

              注意

              通过本SDK,您可以在应用程序的客户端侧跟踪订单状态。但是,我们建议您设置支付Webhook处理程序在应用程序后端接收订单信息。这样您可以实现对完成的购买的额外验证。

              关于Webhook的完整列表及常规使用信息,请参阅Webhook文档

              要在艾克索拉侧配置Webhook:

              1. 在发布商帐户中打开您的项目,然后前往项目设置 > Webhooks部分。
              2. Webhook服务器字段中,输入艾克索拉要向其发送Webhook的URL。

              注:

              进行测试时,可指定https://us-central1-{firebase-project-id}.cloudfunctions.net/webhookFakeResponse,其中{firebase-project-id}是Firebase项目ID(Firebase控制台 > Project Settings > Project ID)。本例中,Firebase模拟了对Webhook的成功处理。对于真实项目,您需要添加购买验证逻辑。

              要测试Webhook,您还可以选择任意专门网站(如webhook.site)或平台(如ngrok)。

              1. 复制并保存密钥字段的值。该密钥默认生成,并用于Webhook签名。如要更改该密钥,请单击更新图标。
              2. 单击启用Webhook

              本文对您的有帮助吗?
              谢谢!
              我们还有其他可改进之处吗? 留言
              非常抱歉
              请说明为何本文没有帮助到您。 留言
              感谢您的反馈!
              我们会查看您的留言并运用它改进用户体验。
              上次更新时间: 2025年12月5日

              发现了错别字或其他内容错误? 请选择文本,然后按Ctrl+Enter。

              报告问题
              我们非常重视内容质量。您的反馈将帮助我们做得更好。
              请留下邮箱以便我们后续跟进
              感谢您的反馈!
              无法发送您的反馈
              请稍后重试或发送邮件至doc_feedback@xsolla.com与我们联系。