通用 Windows 平台 (UWP) 应用可以向客户提供订阅加载项的应用内购买。 可以使用订阅在应用中销售数字产品(如应用功能或数字内容),以及自动定期计费周期。
注意
为了在应用中实现订阅加载项的购买功能,你的项目必须在 Visual Studio 中面向 Windows 10 周年版(10.0; 内部版本 14393) 或更高版本(这对应于 Windows 10 版本 1607),并使用 Windows.Services.Store 命名空间中的 API 实现应用内购买体验,而不是使用 Windows.ApplicationModel.Store 命名空间。 有关这些命名空间之间的差异的详细信息,请参阅 应用内购买和试用。
功能亮点
UWP 应用的订阅加载项支持以下功能:
- 可以选择订阅期限为 1 个月、3 个月、6 个月、1 年或 2 年。
- 可以向订阅添加 1 周或 1 个月的免费试用期。
- Windows SDK 提供了 API,您可以在您的应用中使用,以获取有关可用订阅插件的信息,并启用订阅插件的购买。 我们还提供你可以从你的服务调用的 REST API 来管理用户订阅。
- 可以查看在给定时间段内提供订阅购置数、活动订阅者数和已取消订阅的分析报告。
- 客户可以在Microsoft帐户的 https://account.microsoft.com/services 页上管理其订阅。 客户可以使用此页面查看他们获取的所有订阅、取消订阅并更改与其订阅关联的付款方式。
为应用启用订阅加载项的步骤
若要在应用中启用订阅加载项的购买,请执行以下步骤。
在合作伙伴中心中为你的订阅创建加载项提交,并发布此提交。 在你执行加载项提交流程时,请密切注意以下属性:
产品类型:请确保选择 订阅类型。
订阅周期:选择订阅的定期计费周期。 发布加载项后,无法更改订阅期限。
每个订阅加载项都支持单个订阅周期和试用期。 必须为要在应用中提供的每种订阅类型创建不同的订阅加载项。 例如,如果想要提供无试用版的每月订阅、具有一个月试用版的每月订阅、无试用版的年度订阅,以及具有一个月试用版的年度订阅,则需要创建四个订阅加载项。
试用期:考虑为订阅选择 1 周或 1 个月的试用期,让用户在购买订阅内容之前试用订阅内容。 发布订阅加载项后,无法更改或删除试用期。
若要获取订阅的免费试用版,用户必须通过应用内标准购买过程购买订阅,包括有效的付款方式。 在审判期间,他们不收取任何费用。 试用期结束时,订阅会自动转换为完整的订阅,并且用户的付款方式将在付费订阅的第一个时间段内收费。 如果用户选择在试用期内取消其订阅,则订阅将保持活动状态,直到试用期结束。 某些试用期不适用于所有订阅期。
注意
每个客户只能获取一次订阅加载项的免费试用版。 客户获取订阅的免费试用版后,应用商店会阻止同一客户再次获取相同的免费试用订阅。
可见性:如果要创建测试加载项并且仅用它来测试订阅的应用内购买体验,建议你选择一个在 Microsoft Store 中隐藏选项。 否则,您可以根据您的情境选择最佳可见性选项。
定价:在本部分设定您的订阅价格。 发布加载项后,无法提高订阅的价格。 但是,以后可以降低价格。
重要
默认情况下,当您创建任何加载项时,价格最初设置为 免费。 由于在完成加载项提交后无法提高订阅加载项的价格,因此请务必在此处选择订阅的价格。
在应用中,使用 Windows.Services.Store 命名空间中的 API 来确定当前用户是否已获取订阅加载项,然后将其作为应用内购买提供给用户。 有关更多详细信息,请参阅本文中的 代码示例。
测试你的应用中订阅的应用内购买实现情况。 需要将应用从应用商店下载一次到开发设备,才能使用其许可证进行测试。 有关详细信息,请参阅我们的应用内购买测试指南。
创建并发布一个应用提交,其中包含更新的应用包以及经过测试的代码。 有关详细信息,请参阅应用提交。
代码示例
本节中的代码示例演示如何使用 Windows.Services.Store 命名空间中的 API 获取有关当前应用的订阅加载项的信息,并代表当前用户请求购买订阅加载项。
这些示例具有以下先决条件:
- 适用于面向 Windows 10 周年纪念版(10.0;版本 14393)或更高版本的通用 Windows 平台 (UWP) 应用的 Visual Studio 项目。
- 你已在合作伙伴中心中创建应用提交,并且此应用已在应用商店中发布。 可以选择配置应用,以便在测试应用时无法在应用商店中发现它。 有关详细信息,请参阅 测试指南。
- 你已在合作伙伴中心中为该应用创建了一个订阅加载项。
这些示例中的代码假定:
- 此代码文件包含对 Windows.Services.Store 和 System.Threading.Tasks 命名空间的 using 语句。
- 该应用是一个单用户应用,仅在启动应用的用户的上下文中运行。 有关详细信息,请参阅 应用内购买和试用版。
注意
如果桌面应用程序使用 Desktop Bridge,则可能需要添加这些示例中未显示的其他代码来配置 StoreContext 对象。 有关更多信息,请参阅在使用桌面桥的桌面应用程序中使用 StoreContext 类。
购买订阅加载项
此示例演示如何代表当前客户请求为应用购买已知的订阅加载项。 此示例还演示如何处理订阅有试用期的情况。
- 代码首先确定客户是否已拥有订阅的活动许可证。 如果客户已有活动许可证,则代码应根据需要解锁订阅功能(因为这是应用的专有功能,此示例中会用注释标识)。
- 接下来,代码获取 StoreProduct 对象,该对象代表代客户购买的订阅。 该代码假定你已知道要购买的订阅加载项的 应用商店 ID,并且已将此值分配给 subscriptionStoreId 变量。
- 然后,该代码确定试用版是否可用于订阅。 或者,你的应用可以使用此信息来显示有关客户可用试用版或完整订阅的详细信息。
- 最后,代码调用 RequestPurchaseAsync 方法来请求购买订阅。 如果试用版可用于订阅,则试用版将提供给客户进行购买。 否则,将提供完整的订阅以供购买。
private StoreContext context = null;
StoreProduct subscriptionStoreProduct;
// Assign this variable to the Store ID of your subscription add-on.
private string subscriptionStoreId = "";
// This is the entry point method for the example.
public async Task SetupSubscriptionInfoAsync()
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
bool userOwnsSubscription = await CheckIfUserHasSubscriptionAsync();
if (userOwnsSubscription)
{
// Unlock all the subscription add-on features here.
return;
}
// Get the StoreProduct that represents the subscription add-on.
subscriptionStoreProduct = await GetSubscriptionProductAsync();
if (subscriptionStoreProduct == null)
{
return;
}
// Check if the first SKU is a trial and notify the customer that a trial is available.
// If a trial is available, the Skus array will always have 2 purchasable SKUs and the
// first one is the trial. Otherwise, this array will only have one SKU.
StoreSku sku = subscriptionStoreProduct.Skus[0];
if (sku.SubscriptionInfo.HasTrialPeriod)
{
// You can display the subscription trial info to the customer here. You can use
// sku.SubscriptionInfo.TrialPeriod and sku.SubscriptionInfo.TrialPeriodUnit
// to get the trial details.
}
else
{
// You can display the subscription purchase info to the customer here. You can use
// sku.SubscriptionInfo.BillingPeriod and sku.SubscriptionInfo.BillingPeriodUnit
// to provide the renewal details.
}
// Prompt the customer to purchase the subscription.
await PromptUserToPurchaseAsync();
}
private async Task<bool> CheckIfUserHasSubscriptionAsync()
{
StoreAppLicense appLicense = await context.GetAppLicenseAsync();
// Check if the customer has the rights to the subscription.
foreach (var addOnLicense in appLicense.AddOnLicenses)
{
StoreLicense license = addOnLicense.Value;
if (license.SkuStoreId.StartsWith(subscriptionStoreId))
{
if (license.IsActive)
{
// The expiration date is available in the license.ExpirationDate property.
return true;
}
}
}
// The customer does not have a license to the subscription.
return false;
}
private async Task<StoreProduct> GetSubscriptionProductAsync()
{
// Load the sellable add-ons for this app and check if the trial is still
// available for this customer. If they previously acquired a trial they won't
// be able to get a trial again, and the StoreProduct.Skus property will
// only contain one SKU.
StoreProductQueryResult result =
await context.GetAssociatedStoreProductsAsync(new string[] { "Durable" });
if (result.ExtendedError != null)
{
System.Diagnostics.Debug.WriteLine("Something went wrong while getting the add-ons. " +
"ExtendedError:" + result.ExtendedError);
return null;
}
// Look for the product that represents the subscription.
foreach (var item in result.Products)
{
StoreProduct product = item.Value;
if (product.StoreId == subscriptionStoreId)
{
return product;
}
}
System.Diagnostics.Debug.WriteLine("The subscription was not found.");
return null;
}
private async Task PromptUserToPurchaseAsync()
{
// Request a purchase of the subscription product. If a trial is available it will be offered
// to the customer. Otherwise, the non-trial SKU will be offered.
StorePurchaseResult result = await subscriptionStoreProduct.RequestPurchaseAsync();
// Capture the error message for the operation, if any.
string extendedError = string.Empty;
if (result.ExtendedError != null)
{
extendedError = result.ExtendedError.Message;
}
switch (result.Status)
{
case StorePurchaseStatus.Succeeded:
// Show a UI to acknowledge that the customer has purchased your subscription
// and unlock the features of the subscription.
break;
case StorePurchaseStatus.NotPurchased:
System.Diagnostics.Debug.WriteLine("The purchase did not complete. " +
"The customer may have cancelled the purchase. ExtendedError: " + extendedError);
break;
case StorePurchaseStatus.ServerError:
case StorePurchaseStatus.NetworkError:
System.Diagnostics.Debug.WriteLine("The purchase was unsuccessful due to a server or network error. " +
"ExtendedError: " + extendedError);
break;
case StorePurchaseStatus.AlreadyPurchased:
System.Diagnostics.Debug.WriteLine("The customer already owns this subscription." +
"ExtendedError: " + extendedError);
break;
}
}
获取有关当前应用的订阅加载项的信息
此代码示例演示如何获取应用中提供的所有订阅加载项的信息。 若要获取此信息,请先使用 GetAssociatedStoreProductsAsync 方法获取表示应用每个可用加载项的 StoreProduct 对象的集合。 然后,获取每个产品的 StoreSku,并使用 IsSubscription 和 SubscriptionInfo 属性访问订阅信息。
private StoreContext context = null;
public async Task GetSubscriptionsInfo()
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
// Subscription add-ons are Durable products.
string[] productKinds = { "Durable" };
List<String> filterList = new List<string>(productKinds);
StoreProductQueryResult queryResult =
await context.GetAssociatedStoreProductsAsync(productKinds);
if (queryResult.ExtendedError != null)
{
// The user may be offline or there might be some other server failure.
System.Diagnostics.Debug.WriteLine($"ExtendedError: {queryResult.ExtendedError.Message}");
return;
}
foreach (KeyValuePair<string, StoreProduct> item in queryResult.Products)
{
// Access the Store product info for the add-on.
StoreProduct product = item.Value;
// For each add-on, the subscription info is available in the SKU objects in the add-on.
foreach (StoreSku sku in product.Skus)
{
if (sku.IsSubscription)
{
// Use the sku.SubscriptionInfo property to get info about the subscription.
// For example, the following code gets the units and duration of the
// subscription billing period.
StoreDurationUnit billingPeriodUnit = sku.SubscriptionInfo.BillingPeriodUnit;
uint billingPeriod = sku.SubscriptionInfo.BillingPeriod;
}
}
}
}
从你的服务管理订阅
更新后的应用在应用商店上线,并且当客户可以购买订阅附加组件后,您可能会遇到需要为客户管理订阅的情况。 我们提供可从服务调用的 REST API 来执行以下订阅管理任务:
获取用户有权使用的订阅。 如果你的订阅是跨平台服务的一部分,则可以调用此 API 来确定指定的用户是否具有订阅的权利及其订阅在 UWP 应用的上下文中的状态。 然后,可以使用此信息更新服务支持的其他平台上的订阅状态。
更改给定用户订阅的计费状态。 使用此 API 可取消、扩展或禁用订阅的自动续订。
取消
客户可以使用其Microsoft帐户的 https://account.microsoft.com/services 页查看他们获取的所有订阅、取消订阅和更改与其订阅关联的付款方式。 当客户使用此页取消订阅时,他们将在当前计费周期期间继续有权访问订阅。 对于当前计费周期的任何部分,他们不会获得退款。 在当前计费周期结束时,其订阅将停用。
还可以通过使用 REST API 代表用户取消订阅,更改给定用户订阅的计费状态。
订阅续订和宽限期
在每个计费周期的某个时间点,我们将尝试在下一个计费周期向客户的信用卡收费。 如果收费失败,客户的订阅将进入催缴状态。 这意味着,其订阅在当前计费周期的剩余时间内仍然处于活动状态,但我们会定期尝试为其信用卡收费以自动续订订阅。 此状态最多可以持续两周,直到当前计费周期的结束和下一个计费周期的续订日期。
我们不提供订阅计费的宽限期。 如果我们无法在当前计费周期结束时成功向客户的信用卡收费,则订阅将被取消,并且客户在当前计费周期之后将不再有权访问订阅。
不支持的方案
订阅附加组件当前不支持以下功能。
- 目前不支持直接通过应用商店向客户销售订阅。 订阅仅适用于应用内购买数字产品。
- 客户无法使用Microsoft帐户的“https://account.microsoft.com/services”页切换订阅期。 若要切换到其他订阅期,客户必须取消其当前订阅,然后从应用购买具有不同订阅期的订阅。
- 订阅加载项当前不支持层切换(例如,将客户从基本订阅切换到具有更多功能的高级订阅)。
- 目前订阅加载项不支持销售和促销代码。
- 将订阅加载项的可见性设置为停止获取后续订 现有订阅。 有关更多详细信息,请参阅设置加载项定价和可用性。