# 将网上商店集成到 Unity 游戏中

> 使用经过身份验证的玩家会话从 Unity 游戏打开一个网上商店。

在 Unity 游戏中开设一个网上商店，以便玩家可以从应用内购 (IAP) 目录中购买商品。

网上商店使用您已配置的 IAP 目录和付款提供商。玩家可以在 Web 上完成购买，而无需单独的游戏内店面。

要打开商店，请添加一个游戏内操作（例如按钮），打开商店 URL，并附加玩家的身份验证会话。该会话将玩家标识到网上商店。

## 先决条件##prerequisites

开始之前，请确保您符合以下先决条件：

* 集成[应用内购](/iap.md)的 Unity 项目。
* 项目中的 [Authentication](/authentication.md) SDK（3.7.1 或更高版本）。
* 目标环境中的一个网上商店。

要开设实体店铺，请发布网上商店，使其在 `shop.unity.com/{studio}/game/{slug}` 上可用。要测试草案，只需在目标环境中创建一个网上商店。

有关更多信息，请参阅[创建和发布第一个网上商店](./create-and-publish-your-first-webshop.md)。

## 从游戏中打开网上商店##open-the-webshop-from-your-game

### 使用 SDK helper##use-the-sdk-helper

Unity IAP SDK 提供`RedirectToWebshop`，可让您通过所需的参数和合规性检查来开设网上商店。

通过支付提供商的 `IPaymentProvidersExtendedPurchaseService` 调用 `RedirectToWebshop`。将 `catalogListingId` 留空可打开网上商店首页，或传递列表 ID 可直接打开商品页面：

```csharp
// Open the front page
 UnityIAPServices.StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService.RedirectToWebshop();

// Open a specific product page
 UnityIAPServices.StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService.RedirectToWebshop(catalogListingId: "your-listing-id");
```

#### 在打开之前需要合规性批准##require-compliance-approval-before-opening

要通过合规性检查对网上商店进行门禁，请在调用 `RedirectToWebshop` 之前向 `SetComplianceCheck` 注册回调。回调在每次重定向之前运行。返回 `false` 会取消重定向并使购买失败，`PurchasingUnavailable`：

```csharp
StoreController(PaymentProvider.Name).PaymentProvidersExtendedPurchaseService
    .SetComplianceCheck(async context => await ShowComplianceDialog(context));
```

#### 控制网上商店的打开方式##control-how-the-webshop-opens

默认情况下，SDK 在外部浏览器中打开网上商店。要更改此设置，请在重定向之前在支付提供商的`IPaymentProvidersExtendedPurchaseService`上调用 `SetWebshopPresentationMode(CheckoutPresentationMode)`。此设置独立于 `SetCheckoutPresentationMode`。

### 手动集成##manual-integration

如果您无法使用 SDK helper，请直接构建并打开 Webshop URL。

要通过按钮或其他游戏内操作来打开网上商店，请从网上商店服务解析商店 URL，然后使用 `Application.OpenURL` 打开 URL。

在运行时解析 URL 可以让同一版本打开已发布的商店或环境草案预览。这样可以在发布之前测试草案。

要解析 URL，请向 `storefront-link` 终端发送`GET`请求：

```text
https://webshop.services.api.unity.com/v1/projects/{projectId}/environments/{environmentId}/storefront-link
```

默认情况下，服务会根据环境状态自动返回 URL：

* 发布的生产环境会返回实时公共店面 URL。实时 URL 会匿名解析。
* 非生产环境或未发布的生产环境会返回短暂的预览草案 URL。

要预览真实商店的未发布更改，请添加 `source=draft` 查询参数。这将返回预览 URL 草案，即使对于已发布的生产环境也是如此：

```text
https://webshop.services.api.unity.com/v1/projects/{projectId}/environments/{environmentId}/storefront-link?source=draft
```

集成使用以下 Authentication SDK 令牌：

* 解析预览草案时，访问令牌会授权`storefront-link`请求。在 `Authorization: Bearer` 标头中发送访问令牌。
* 会话令牌在浏览器中对玩家进行身份验证。在开店前使用 `GenerateRestrictedTokenAsync` 标记创建短暂的一次性受限令牌，然后将会话令牌作为`sessionToken`查询参数附加到解析的 URL。

解析 URL 后，请附加会话令牌、项目 ID 和环境，以便商店为正确的玩家打开。

有关每个查询参数以及商店在缺少参数时的行为方式的更多信息，请参阅[从游戏打开网上](./deep-links.md#outbound-links)商店。

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Authentication;
using UnityEngine;
using UnityEngine.Networking;

public class WebshopLauncher :MonoBehaviour
{
    // Replace these with the values from your Unity Cloud project and webshop configuration.
    const string ProjectId       = "<your-project-id>";
    const string EnvironmentId = "<your-environment-id>"; // 在 API 请求路径中使用
    const string EnvironmentName = "production"; // 在商店 URL 中使用

    const string StorefrontLinkEndpoint =
        "https://webshop.services.api.unity.com/v1/projects/{0}/environments/{1}/storefront-link";

    // draftPreview: request source=draft to preview unpublished changes even on a live env.
    public void OpenShop(string locale, string currency, bool draftPreview = false)
    {
        StartCoroutine(OpenShopRoutine(locale, currency, draftPreview));
    }

    IEnumerator OpenShopRoutine(string locale, string currency, bool draftPreview)
    {
        var signedIn = AuthenticationService.Instance.IsSignedIn;

        // The access token is required for draft previews; live storefronts open anonymously
        // and ignore any token sent.每当玩家登录时发送。
        if (draftPreview && !signedIn)
        {
            Debug.LogWarning("WebshopLauncher: draft preview requires the player to be signed in.");
            return break;
        }

        // 1.向 Webshop 服务询问店面 URL。
        var endpoint = string.Format(StorefrontLinkEndpoint, ProjectId, EnvironmentId);
        if (draftPreview)
            endpoint += "?source=draft";

        using var request = UnityWebRequest.Get(endpoint);
        request.SetRequestHeader("Accept", "application/json");
        if (signedIn)
            request.SetRequestHeader("Authorization", $"Bearer {AuthenticationService.Instance.AccessToken}");

        yield return request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"WebshopLauncher: storefront-link request failed" +
                           $"({request.responseCode}): {request.error}");
            return break;
        }

        var link = JsonUtility.FromJson<StorefrontLinkResponse>(request.downloadHandler.text);
        if (link == null || string.IsNullOrEmpty(link.storefrontUrl))
        {
            Debug.LogError("WebshopLauncher: storefront-link response did not contain a storefrontUrl.");
            return break;
        }

        // 2.为 Webshop 重定向生成一个短暂的一次性受限令牌。
        var tokenOptions = new RestrictedTokenOptions
        {
            Services = new List<string> { "no-svc" }，// ID 令牌无法用于任何实际服务
            SingleUse = true，// 第一次刷新时由网上商店消耗
            TtlSeconds = 60， // 重定向前已最小化
        };

        var tokenTask = AuthenticationService.Instance.GenerateRestrictedTokenAsync(tokenOptions);
        yield return new WaitUntil(() => tokenTask.IsCompleted);

        if (tokenTask.IsFaulted)
        {
            Debug.LogError($"WebshopLauncher: failed to generate restricted token: {tokenTask.Exception}");
            return break;
        }

        // 3.打开商店。
        var sessionToken = tokenTask.Result.SessionToken;
        var shopUrl = BuildShopUrl(link.storefrontUrl, sessionToken, locale, currency);

        Application.OpenURL(shopUrl);
    }

    static string BuildShopUrl(string storefrontUrl, string sessionToken, string locale, string currency)
    {
        var url = storefrontUrl;
        url = AppendParam(url, "sessionToken", sessionToken);
        url = AppendParam(url, "projectId", ProjectId);
        url = AppendParam(url, "environment", EnvironmentName);
        url = AppendParam(url, "locale", locale);
        url = AppendParam(url, "currency", currency);
        return url;
    }

    static string AppendParam(string url, string key, string value)
    {
        if (string.IsNullOrEmpty(value))
            return url;

        var separator = url.Contains("?") ? '&' : '?';
        return $"{url}{separator}{key}={UnityWebRequest.EscapeURL(value)}";
    }

    [可序列化]
    class StorefrontLinkResponse
    {
        public string storefrontUrl;
        public bool live;
    }
}
```

商店打开后，它会从 URL 中删除这些参数，并将玩家的会话保留在浏览器中。有关会话和持久性的更多信息，请参阅[从游戏打开网上](./deep-links.md#authenticate-players-and-persist-sessions)商店。

> **Warning:**
>
> 儿童隐私儿童数据法，包括但不限于美国的《儿童网络隐私保护法》(COPPA)，对如何收集和使用年龄受限用户（例如，13 岁、16 岁或 18 岁以下儿童，具体年龄取决于适用法律）的数据做出了限制。除非符合 [Unity 服务条款](https://unity.com/legal/terms-of-service)中概述的适用法律，否则您不会向 Unity 传输属于年龄限制用户的任何“个人信息”。

如果没有要传递的区域设置或货币，请省略这些参数。默认情况下，IAP 目录使用玩家的浏览器区域设置，默认为美元 (USD)。有关目录区域设置处理的更多信息，请参阅 [Catalog and payments in webshops](./catalog-and-payments.md)（目录和付款）。

## 处理入站深层链接##handle-inbound-deep-links

为网上商店设置 **Deeplink URL** 时，商店会通过该自定义 URL 方案将玩家返回到游戏中。商店会在购买后以及玩家在未验证的登录页面上选择 **Connect to game** 时使用返回深层链接。

要接收返回深层链接，请在设备上注册自定义 URL 方案，并在运行时处理传入链接。有关商店如何构建返回 URL 的更多信息，请参阅[从游戏打开网上](./deep-links.md#post-purchase-return-to-the-game)商店。

> **Note:**
>
> 对于**连接到游戏**流程，Unity IAP SDK 会自动重新打开网上商店。您不需要自己处理该链接。
>
> 确保满足以下要求：
>
> * 玩家在初始化 IAP 之前登录 Unity Authentication。
> * PaymentProvider 商店已连接。
>
> 处理购买后退货使用标准平台自定义 URL 方案处理，不需要特定于 Unity 的 SDK。

> **Note:**
>
> 仅在构建的播放器中返回深层链接解析，而不是在 Unity Editor 或 WebGL 中。Editor 未注册为自定义方案的处理程序，因此从浏览器打开的链接永远不会进入播放模式，WebGL 构建将使用其页面 URL 而不是自定义方案。在设备（iOS 或 Android）或独立版本上测试返回流程。有关平台支持和注册详细信息，请参阅 Unity 的[深度链接](https://docs.unity3d.com/Manual/deep-linking.html)手册。

### 注册 URL 方案##register-the-url-scheme

声明与在 Dashboard（后台）中的 **Deeplink URL**（深度链接 URL）字段中设置的方案相同。操作系统使用 方案将链接路由到您的游戏。

* **iOS 和 macOS**：在 **Edit > Project Settings > Player > Other Settings > Supported URL schemes** 下添加方案。Unity 在构建时将其写入构建的应用程序的`Info.plist` (`CFBundleURLTypes`)。最好不要手动编辑生成的`Info.plist`，因为每次构建都会重新生成。
* **Android**通过自定义主清单或 Gradle 清单模板，将带有`<data android:scheme="mygame" />`条目的`intent-filter`添加到您的活动中。

方案必须与 **Deeplink URL** 值完全匹配。更改 Android 清单后，重新安装游戏，以便操作系统选择新方案。

### 在运行时处理链接##handle-the-link-at-runtime

订阅 `Application.deepLinkActivated`，了解游戏运行时到达的链接。在启动时，还选中 `Application.absoluteURL` 来处理冷启动，即深层链接启动游戏的位置。

```csharp
void Awake()
{
    // Links that arrive while the game is running.
    Application.deepLinkActivated += OnReturnFromWebshop;

    // Cold start: the deep link launched the game.
    if (!string.IsNullOrEmpty(Application.absoluteURL))
        OnReturnFromWebshop(Application.absoluteURL);
}

void OnReturnFromWebshop(string url)
{
    // Handle the post-purchase return: the shop appends ?status=success
    // (and playerId when available) after a completed purchase.
    if (new Uri(url).Query.Contains("status=success"))
    {
        // Purchase completed on the web — refresh the player's entitlements.
    }
    // The Connect to game sign-in link is reopened by the SDK automatically,
    // so it needs no handling here.
}
```

商店仅发送`status=success`，因此请在此处处理该案例。SDK 会自动重新打开 **Connect to game** 登录链接，如上一条中所述。

## 试开店##test-opening-the-shop

在发布之前测试预览草案，然后对真实商店重复测试。要测试草案，请使用 `draftPreview: true` 调用 `OpenShop` 并确认玩家已登录。您可以以非生产环境为目标，或使用 `source=draft` 预览生产环境中未发布的更改。

要验证集成，请执行以下步骤：

1. 在移动设备上构建并安装游戏。
2. 触发调用 `OpenShop` 的游戏内按钮以启动设备的系统浏览器。
3. 确认浏览器打开到解析的店面 URL，并在 URL 中使用您的区域和货币。草案将打开环境范围的预览 URL。`shop.unity.com/{studio}/game/{slug}` 开直播店。
4. 确认商店打开时显示的是经过身份验证的商品列表，而不是未经过身份验证的登录页面。
5. 完成沙盒购买。请参阅相关 IAP 付款提供商的沙盒文档以了解测试凭据。
6. 当草案按预期工作时，请向 `OpenShop` 发出 `draftPreview: false` 电话，然后针对实际发布的商店重复测试。

如果商店打开时显示的是未经身份验证的登录页面而不是商品列表，请确认游戏通过了 URL 的有效`sessionToken`和`projectId`。请参阅 [Webshop 故障排除](./troubleshooting.md)。
