Mobile-Buy-SDK 13.0.0

Mobile-Buy-SDK 13.0.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2024年7月
SPM 支持 SPM

Shopify维护。



  • Shopify Inc.

Mobile Buy SDK

Tests GitHub Release Carthage compatible Swift Package Manager compatible GitHub license

Mobile Buy SDK

Mobile Buy SDK 可以轻松地在您的移动应用程序中创建自定义店面,用户可以使用 Apple Pay 或信用卡购买产品。SDK 通过 GraphQL 连接到 Shopify 平台,并支持广泛的本地店面体验。

目录

文档

您可以通过运行Documentation方案生成完整的HTML和.docset文档。然后,您可以使用文档浏览器,例如Dash来访问.docset工件,或直接在Docs/BuyDocs/Pay目录中浏览HTML。

此文档使用Jazzy生成。

安装

下载最新版本

Swift 包管理器

这是将SDK与您的应用程序集成的推荐方法。您可以按照Apple的指南,在应用中添加包依赖项来进行详细操作。

动态框架安装 🔼

  1. 通过运行以下命令将Buy作为git子模块添加:
git submodule add [email protected]:Shopify/mobile-buy-sdk-ios.git
  1. 确保已经通过运行以下命令更新了Buy SDK的所有子模块:
git submodule update --init --recursive
  1. Buy.xcodeproj拖入您的应用程序项目中。
  2. Buy.framework作为依赖项添加
    1. 导航到构建阶段 > 目标依赖项
    2. 添加Buy.framework
  3. 链接Buy.framework
    1. 导航到构建阶段 > 与库链接二进制
    2. 添加Buy.framework
  4. 确保框架已复制到应用程序包中
    1. 导航到构建阶段 > 新建复制文件阶段
    2. 目标下拉列表中选择框架
    3. 添加Buy.framework
  5. 使用import Buy将它们导入到项目文件中。

查看Storefront示例应用,了解如何将Buy目标添加为依赖项的示例。

Carthage 🔼

  1. 将以下行添加到您的Cartfile中:
github "Shopify/mobile-buy-sdk-ios"
  1. 运行carthage update
  2. 按照链接Carthage生成的动态框架的步骤进行操作。
  3. 导入SDK模块
import Buy

CocoaPods 🔼

  1. 将以下行添加到您的Podfile中:
pod "Mobile-Buy-SDK"
  1. 运行pod install
  2. 导入SDK模块
import MobileBuySDK

注意:如果您已对该存储库进行分支并尝试从您的git目标、提交或分支安装,请确保在Podfile的行中包含“submodules: true”

开始使用

Buy SDK建立在GraphQL之上。SDK处理所有查询生成和响应解析,仅公开类型化模型和编译时检查的查询结构。它不需要你编写字符串查询或解析JSON响应。

你不需要成为GraphQL专家就可以开始使用Buy SDK(但如果你之前使用过它将有所帮助)。下面的部分提供了对这个系统的简要介绍,以及一些如何使用它来构建安全自定义店铺的例子。

从SDK v2.0迁移

之前版本的Mobile Buy SDK(版本2.0)基于REST API。在3.0版中,Shopify正在将SDK从REST迁移到GraphQL。

不幸的是,由于生成GraphQL模型的细节,几乎不可能从v2.0迁移到v3.0(领域模型不向后兼容)。但是,两个版本中的主要概念是相同的,例如集合、产品、结账和订单。

代码生成

Buy SDK建立在生成类层次结构之上,这些类用于构建和解析GraphQL查询和响应。这些类通过运行一个依赖于GraphQL Swift Generation库的定制Ruby脚本来手动生成。大多数生成功能和支持类都位于该库中。它通过下载GraphQL模式、生成Swift类层次结构和将生成的文件保存到指定的文件夹路径来完成工作。此外,它还提供了自定义GraphQL标量类型(如DateTime)的覆盖。

请求模型

所有生成的请求模型均派生于 GraphQL.AbstractQuery 类型。尽管此抽象类型包含构建查询所需的所有功能,但您不应直接使用它。相反,应依赖于生成的子类中提供的类型化方法。

以下示例显示了一个查询商店名称的示例查询

let query = Storefront.buildQuery { $0
    .shop { $0
        .name()
    }
}

切勿直接使用抽象类

// Never do this

let shopQuery = GraphQL.AbstractQuery()
shopQuery.addField(field: "name")

let query = GraphQL.AbstractQuery()
query.addField(field: "shop", subfields: shopQuery)

两个查询都生成相同的GraphQL查询(见下文),但前一种方法提供了自动完成和对GraphQL模式进行编译时验证的功能。如果请求的字段不存在、类型不正确或已弃用,它会报错。您也可能已经注意到,前一种方法类似于GraphQL查询语言结构(这是故意的)。查询既易于编写又更易于阅读。

query {
  shop {
    name
  }
}

响应模型

所有生成的响应模型都派生于 GraphQL.AbstractResponse 类型。此抽象类型为通过类访问GraphQL响应中的字段值提供了类似于 Dictionary 的键值类型接口。就像 GraphQL.AbstractQuery 一样,您不应直接使用这些访问器,而应依赖于生成的子类中的类型化派生属性。

以下示例基于前面关于访问商店名称查询结果的示例

// response: Storefront.QueryRoot

let name: String = response.shop.name

切勿直接使用抽象类

// Never do this

let response: GraphQL.AbstractResponse

let shop = response.field("shop") as! GraphQL.AbstractResponse
let name = shop.field("name") as! String

同样,两种方法都会产生相同的结果,但首选前一种情况:它不需要进行类型转换,因为它已经知道预期的类型。

Node 协议

GraphQL架构定义了一个Node接口,该接口在任何符合类型上声明了一个id字段。这使得在只有id的情况下查询架构中的任何对象变得方便。这个概念也被带到了Buy SDK中,但需要将数据类型转换到正确的类型。您需要确保Node类型是正确的,否则将数据类型转换为错误类型将返回运行时异常。

给定这个查询

let id    = GraphQL.ID(rawValue: "NkZmFzZGZhc")
let query = Storefront.buildQuery { $0
    .node(id: id) { $0
        .onOrder { $0
            .id()
            .createdAt()
        }
    }
}

Storefront.Order需要一个类型转换

// response: Storefront.QueryRoot

let order = response.node as! Storefront.Order

别名

当单一查询需要请求同一层级的多个同名字段时,别名非常有用,因为GraphQL仅允许唯一字段名。可以通过为每个节点使用唯一的别名来查询多个节点

let query = Storefront.buildQuery { $0
    .node(aliasSuffix: "collection", id: GraphQL.ID(rawValue: "NkZmFzZGZhc")) { $0
        .onCollection { $0
            // fields for Collection
        }
    }
    .node(aliasSuffix: "product", id: GraphQL.ID(rawValue: "GZhc2Rm")) { $0
        .onProduct { $0
            // fields for Product
        }
    }
}

访问别名的节点与普通的节点类似

// response: Storefront.QueryRoot

let collection = response.aliasedNode(aliasSuffix: "collection") as! Storefront.Collection
let product    = response.aliasedNode(aliasSuffix: "product")    as! Storefront.Product

了解有关GraphQL别名的更多信息。

Graph.Client

Graph.Client是在URLSession之上构建的一个网络层,用于执行查询提交请求。它还简化了轮询和重试请求。要开始使用Graph.Client,您需要以下内容

  • 您的商店的.myshopify.com域名
  • 您的API密钥,可以在您的商店管理页面找到
  • 一个URLSession(可选),如果要自定义用于网络请求的配置或有意愿将现有的URLSessionGraph.Client共享
  • (可选)买家的当前地区。支持的区域限制在您的商店可用的区域。
let client = Graph.Client(
	shopDomain: "shoes.myshopify.com",
	apiKey:     "dGhpcyBpcyBhIHByaXZhdGUgYXBpIGtleQ"
)

如果您的商店支持多种语言,则Storefront API可以返回已翻译的资源类型和字段。了解有关内容翻译的更多信息。

// Initializing a client to return translated content
let client = Graph.Client(
	shopDomain: "shoes.myshopify.com",
	apiKey:     "dGhpcyBpcyBhIHByaXZhdGUgYXBpIGtleQ",
        locale:     Locale.current
)

GraphQL指定了两种操作类型:查询和提交。客户端将这些暴露为两种类型安全的操作,尽管它还为每个操作提供了一些重试和轮询的便利。

查询

在语义上,GraphQL的query操作与RESTful风格的GET请求相当。它保证了服务器上不会突变资源。使用Graph.Client,您可以使用以下方式执行查询操作:

public func queryGraphWith(_ query: Storefront.QueryRootQuery, retryHandler: RetryHandler<Storefront.QueryRoot>? = default, completionHandler: QueryCompletion) -> Task

以下示例展示了如何查询商店的名称

let query = Storefront.buildQuery { $0
    .shop { $0
        .name()
    }
}

let task = client.queryGraphWith(query) { response, error in
    if let response = response {
        let name = response.shop.name
    } else {
        print("Query failed: \(error)")
    }
}
task.resume()

了解更多关于GraphQL查询的信息。

变更

在语义上,GraphQL的mutation操作与RESTful风格的PUTPOSTDELETE请求相当。变更通常伴随着代表要更新值的输入和用于获取已更新资源字段的查询。您可以将mutation视为两步操作,其中资源首先被修改,然后使用提供的query进行查询。操作的第二部分与常规的query请求相同。

使用Graph.Client您可以通过以下方式执行变更操作:

public func mutateGraphWith(_ mutation: Storefront.MutationQuery, retryHandler: RetryHandler<Storefront.Mutation>? = default, completionHandler: MutationCompletion) -> Task

以下示例展示了如何使用恢复令牌重置客户的密码

let customerID = GraphQL.ID(rawValue: "YSBjdXN0b21lciBpZA")
let input      = Storefront.CustomerResetInput.create(resetToken: "c29tZSB0b2tlbiB2YWx1ZQ", password: "abc123")
let mutation   = Storefront.buildMutation { $0
    .customerReset(id: customerID, input: input) { $0
        .customer { $0
            .id()
            .firstName()
            .lastName()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

let task = client.mutateGraphWith(mutation) { response, error in
    if let mutation = response?.customerReset {

        if let customer = mutation.customer, !mutation.userErrors.isEmpty {
            let firstName = customer.firstName
            let lastName = customer.lastName
        } else {

            print("Failed to reset password. Encountered invalid fields:")
            mutation.userErrors.forEach {
                let fieldPath = $0.field?.joined() ?? ""
                print("  \(fieldPath): \($0.message)")
            }
        }

    } else {
        print("Failed to reset password: \(error)")
    }
}
task.resume()

变更通常依赖于某种用户输入。尽管您应该在发布变更之前始终验证用户输入,但是对于动态数据来说,永远没有保证。因此,您应始终在变更操作中请求userErrors字段(如果可用),以便在UI中提供有关遇到的任何问题的有用反馈。这些错误可能包括从无效的电子邮件地址密码太短的各种问题。

了解更多关于GraphQL变更的信息。

重试

queryGraphWithmutateGraphWith 都接受一个可选的 RetryHandler<R: GraphQL.AbstractResponse>。该对象包含重试状态和配置参数,用于确定 Client 如何重试后续请求(例如在设置延迟或重试次数后)。默认情况下,retryHandler 为 nil,不会提供任何重试行为。要启用重试或轮询,需要创建一个带有条件的处理器。如果 handler.conditionhandler.canRetry 计算结果为 true,那么 Client 将继续执行请求。

let handler = Graph.RetryHandler<Storefront.QueryRoot>() { (query, error) -> Bool in
    if myCondition {
        return true // will retry
    }
    return false // will complete the request, either succeed or fail
}

重试处理器是通用的,可以同等良好地处理 querymutation 请求。

缓存

网络查询和变更既可能很低效,也可能代价很高。对于不太经常改变的资源,您可能希望使用缓存来帮助减少带宽和延迟。由于 GraphQL 依赖于 POST 请求,我们无法充分利用 URLSession 中可用的 HTTP 缓存。因此,Graph.Client 接收了一个可选的缓存层,可以进行客户端整个范围的启用或根据每次请求启用。

重要: 缓存只适用于 query 操作。它不适用于 mutation 操作或任何其他请求,这些请求提供了 retryHandler

有四种可用的缓存策略

  • .cacheOnly - 只从缓存中获取响应,忽略网络。如果缓存的响应不存在,则返回错误。
  • .networkOnly - 仅从网络中获取响应,忽略任何缓存响应。
  • .cacheFirst(expireIn: Int) - 首先从缓存中获取响应。如果响应不存在或超过 expireIn 的年龄,则从网络获取响应。
  • .networkFirst(expireIn: Int) - 首先从网络中获取响应。如果网络失败并且缓存的响应不超过 expireIn 的年龄,则返回缓存数据。

启用全局缓存

可以通过为 Graph.Client 的任何实例提供默认的 cachePolicy 来启用客户端的全局缓存。这将设置所有 query 操作使用默认缓存策略,除非为单个请求指定了不同的策略。

在这个例子中,我们将客户端的 cachePolicy 属性设置为 cacheFirst

let client = Graph.Client(shopDomain: "...", apiKey: "...")
client.cachePolicy = .cacheFirst

现在,所有对 queryGraphWith 的调用都将返回一个带有 .cacheFirst 缓存策略的任务。

如果您想为一个单独的请求覆盖客户端的全局缓存策略,请将不同的缓存策略作为 queryGraphWith 参数指定。

let task = client.queryGraphWith(query, cachePolicy: .networkFirst(expireIn: 20)) { query, error in
    // ...
}

在这个示例中,task 缓存策略更改为 .networkFirst(expireIn: 20),这意味着缓存的响应从收到响应的时间起有效 20 秒。

错误

无论是 查询 还是 突变 请求的完成,都将始终包含一个可选的 Graph.QueryError,它表示请求的当前错误状态。请注意,errorresponse 并非互相排斥。存在错误和响应都是完全有效的。错误的存在可以代表网络错误(例如网络错误或无效的 JSON)或 GraphQL 错误(例如无效的查询语法或缺少参数)。Graph.QueryError 是一个 枚举,因此检查错误的类型是极其简单的。

let task = client.queryGraphWith(query) { response, error in
    if let response = response {
        // Do something
    } else {

        if let error = error, case .http(let statusCode) = error {
            print("Query failed. HTTP error code: \(statusCode)")
        }
    }
}
task.resume()

如果错误类型为 .invalidQuery,则返回一个 Reason 对象的数组。这些将提供有关查询错误的更深入信息。请注意,这些错误不是为了向最终用户显示而设计的。它们仅用于调试目的。

以下示例展示了无效查询的 GraphQL 错误响应

{
  "errors": [
    {
      "message": "Field 'Shop' doesn't exist on type 'QueryRoot'",
      "locations": [
        {
          "line": 2,
          "column": 90
        }
      ],
      "fields": ["query CollectionsWithProducts", "Shop"]
    }
  ]
}

了解更多关于 GraphQL 错误 的信息。

搜索

一些 Storefront 模型通过 query 参数接受搜索词。例如,您可以通过 query 提供一个搜索集合,这些集合包含任何字段中的特定搜索词。

以下示例展示了如何找到包含单词 "shoes" 的集合

let query = Storefront.buildQuery { $0
    .shop { $0
        .collections(first: 10, query: "shoes") { $0
            .edges { $0
                .node { $0
                    .id()
                    .title()
                    .description()
                }
            }
        }
    }
}

模糊匹配

在上面的示例中,查询是 shoes。这将匹配描述、标题和其他字段包含 "shoes" 的集合。这是查询的最简单形式。它提供了对集合所有字段的搜索词的模糊匹配。

字段匹配

作为对全局模糊匹配的替代,您还可以指定要包含在搜索中的单个字段。例如,如果您想匹配特定类型的集合,您可以通过指定字段来实现

.collections(first: 10, query: "collection_type:runners") { $0
    ...
}

指定字段和搜索参数的格式如下: 字段:搜索词。请注意,在 搜索词 之间不能有空格。支持搜索的字段已在 Buy SDK 生成接口中记录。

重要:如果您在搜索中指定了字段(如上例所示),则 搜索词 将是 精确匹配 而非模糊匹配。例如,根据上述查询,类型为 blue_runners 的集合将不会匹配 runners 的查询。

否定字段匹配

每个搜索字段也可以被否定。基于上述示例,如果您想匹配所有不是 runners 类型集合,可以在相关字段后面追加一个 -

.collections(first: 10, query: "-collection_type:runners") { $0
    ...
}

布尔运算符

除了单字段搜索之外,您还可以使用布尔运算符构建更复杂的搜索。它们与普通的 SQL 运算符非常相似。

以下示例显示了如何搜索带有 blue 标签且类型为 sneaker 的产品

.products(first: 10, query: "tag:blue AND product_type:sneaker") { $0
    ...
}

您还可以对搜索词进行分组

.products(first: 10, query: "(tag:blue AND product_type:sneaker) OR tag:red") { $0
    ...
}

比较运算符

搜索语法还可以比较不匹配的值。例如,您可能希望获取仅在某个日期之后更新的产品。您也可以这样做

.products(first: 10, query: "updated_at:>\"2017-05-29T00:00:00Z\"") { $0
    ...
}

上述查询将返回在2017年5月29日午夜之后更新的产品。请注意,日期被另一对转义引号所包围。您也可以将此技术用于多个单词或句子。

SDK 支持以下比较运算符

  • : 等于
  • :< 小于
  • :> 大于
  • :<= 小于等于
  • :>= 大于等于

重要: := 不是一个有效的运算符。

存在运算符

有一个特殊的运算符可以用来检查 null 或空值。

以下示例显示如何找到没有任何标签的产品。您可以使用 * 运算符和取消字段的引用来这样做

.products(first: 10, query: "-tag:*") { $0
    ...
}

卡金库

Buy SDK 通过 GraphQL 支持原生动行结账,允许您使用信用卡完成结账。但是,它不接受直接信用卡号码。相反,您需要通过独立的、符合PCI标准的网台服务来存储信用卡信息。Buy SDK 使用 Card.Client 使这项操作变得简单。

卡客户端

如同 Graph.ClientCard.Client 管理与提供不透明信用卡标记的卡服务器的交互。这些标记用于完成结账。在以安全方式收集用户的信用卡信息后,创建一个信用卡表示形式,并提交一个保险库请求

// let settings: Storefront.PaymentSettings
// let cardClient: Card.Client

let creditCard = Card.CreditCard(
	firstName:        "John",
	middleName:       "Singleton",
	lastName:         "Smith",
	number:           "1234567812345678",
	expiryMonth:      "07",
	expiryYear:       "19",
	verificationCode: "1234"
)

let task = cardClient.vault(creditCard, to: settings.cardVaultUrl) { token, error in
    if let token = token {
        // proceed to complete checkout with `token`
    } else {
        // handle error
    }
}
task.resume()

重要:信用卡保险库服务不提供提交的信用卡的任何验证。因此,提交无效的信用卡号码或遗漏字段始终会产生保险库 标记。任何与无效信用卡信息相关的错误仅在提供的 标记 被用于完成结账时才会显示。

Apple Pay

对 Pay 的支持由 Pay 框架提供。它与 Buy SDK 独立编译和测试,并为支持您的应用程序中的  Pay 提供一个更简单的接口。它旨在消除使用带有 PKPaymentAuthorizationController 的部分 GraphQL 模型时的猜测工作。

支付会话

当客户准备好使用  Pay 在您的应用程序中购买产品时,PaySession 封装了完成结账过程所需的所有必要状态

  • 您的商店货币
  • 您的  Pay 商户ID
  • 可用的运费率
  • 选定的运费率
  • 账单和发货地址
  • 结账状态

要显示  Pay 模态并开始结账过程,您需要

  • 已创建的 Storefront.Checkout
  • 可以从 Storefront.Shop 上的查询获取的货币信息
  •  Pay 商户ID

在满足所有先决条件后,您可以初始化一个 PaySession 并开始支付授权过程

self.paySession = PaySession(
	checkout:   payCheckout,
	currency:   payCurrency,
	merchantID: "com.merchant.identifier"
)

self.paySession.delegate = self
self.paySession.authorize()

在调用authorize()后,会代您创建一个PKPaymentAuthorizationController,并将其展示给客户。通过提供一个delegate,客户更改送货地址、选择送货方式和使用TouchID或密码授权支付时,您会收到通知。正确处理每个事件,通过更新Storefront.Checkout的适当突变,是至关重要的。这确保了服务器上的结账状态保持最新。

让我们一一来看

func paySession(_ paySession: PaySession, didSelectShippingRate shippingRate: PayShippingRate, checkout: PayCheckout, provide: @escaping  (PayCheckout?) -> Void) {

    self.updateCheckoutWithSelectedShippingRate(id: checkout.id, shippingRate: shippingRate) { updatedCheckout in
        if let updatedCheckout = updatedCheckout {

            /* Be sure to provide the update checkout that include the shipping
             * line selected by the customer.
             */
            provide(updatedCheckout)

        } else {

            /* By providing a nil checkout we inform the PaySession that we failed
             * to select the shipping rate for this checkout. The PaySession will
             * fail the current payment authorization process and a generic error
             * will be shown to the customer.
             */
            provide(nil)
        }
    }
}

每当客户选择不同的送货方式以及首次更新送货费用(由于上一个delegate回调)时调用。

func paySession(_ paySession: PaySession, didAuthorizePayment authorization: PayAuthorization, checkout: PayCheckout, completeTransaction: @escaping (PaySession.TransactionStatus) -> Void) {

    /* 1. Update checkout with complete shipping address. Example:
     * self.updateCheckoutShippingAddress(id: checkout.id, shippingAddress: authorization.shippingAddress) { ... }
     *
     * 2. Update checkout with the customer's email. Example:
     * self.updateCheckoutEmail(id: checkout.id, email: authorization.shippingAddress.email) { ... }
     *
     * 3. Complete checkout with billing address and payment data
     */
     self.completeCheckout(id: checkout.id, billingAddress: billingAddress, token: authorization.token) { success in
         completeTransaction(success ? .success : .failure)
     }
}

当客户授权支付时调用。此时,delegate将接收到加密的token和其他相关信息,这些信息您完成购买所需的completeCheckout突变。在调用最终结账完成突变之前,服务器上的结账状态必须是最新的。确保在完成结账前所有正在进行的更新突变都已完成。

完成支付

func paySessionDidFinish(_ paySession: PaySession) {
    // Do something after the  Pay modal is dismissed
}

在关闭钱包模态时被调用,不管支付授权是否成功。

案例研究

在使用任何SDK时可能会有一些困惑。本章节旨在探索所有可能需要在iOS上构建自定义店面时使用Buy SDK的区域,并为您的实现提供一个稳固的起点。

在本章节中,我们假设您已在源代码的某处设置了客户端。尽管可以拥有多个 Graph.Client 实例,但重用单个实例提供许多幕后性能改进。

let client: Graph.Client

获取商店信息

在向用户展示产品之前,通常需要获取商店的多种元数据。这可能是货币代码,甚至是商店名称。

let query = Storefront.buildQuery { $0
    .shop { $0
        .name()
    }
}

let task = client.queryGraphWith(query) { response, error in
    let name         = response?.shop.name
}
task.resume()

相应的GraphQL查询看起来像这样

query {
  shop {
    name
  }
}

获取集合和产品

在我们提供的自定义店面样本中,我们希望展示一个包含多个产品预览的集合。使用传统的RESTful服务,这需要一次网络请求用于集合,然后对于数组中的每个集合都要进行一次网络请求。这通常被称为n + 1问题。

Buy SDK是基于GraphQL构建的,它解决了n + 1请求问题。在下面的示例中,单个查询通过一次网络请求检索10个集合以及每个集合中10个产品。

let query = Storefront.buildQuery { $0
    .shop { $0
        .collections(first: 10) { $0
            .edges { $0
                .node { $0
                    .id()
                    .title()
                    .products(first: 10) { $0
                        .edges { $0
                            .node { $0
                                .id()
                                .title()
                                .productType()
                                .description()
                            }
                        }
                    }
                }
            }
        }
    }
}

let task = client.queryGraphWith(query) { response, error in
    let collections  = response?.shop.collections.edges.map { $0.node }
    collections?.forEach { collection in

        let products = collection.products.edges.map { $0.node }
    }
}
task.resume()

相应的GraphQL查询看起来像这样

{
  shop {
    collections(first: 10) {
      edges {
        node {
          id
          title
          products(first: 10) {
            edges {
              node {
                id
                title
                productType
                description
              }
            }
          }
        }
      }
    }
  }
}

由于它仅检索每个资源的一小部分属性,这种GraphQL调用也比通过传统的REST检索100个完整资源要节省带宽。

但是,如果你需要获取每个集合中的10个以上的产品呢?

分页

尽管假设单个网络请求足以加载所有集合和产品可能很方便,但这可能是一种天真的看法。最佳实践是对结果进行分页。由于Buy SDK建立在GraphQL之上,它继承了edgesnodes的概念。

更多关于GraphQL中的分页的信息。

以下示例展示了如何在集合中分页获取产品

let query = Storefront.buildQuery { $0
    .node(id: collectionID) { $0
        .onCollection { $0
            .products(first: 10, after: productsCursor) { $0
                .pageInfo { $0
                    .hasNextPage()
                }
                .edges { $0
                    .cursor()
                    .node { $0
                        .id()
                        .title()
                        .productType()
                        .description()
                    }
                }
            }
        }
    }
}

let task = client.queryGraphWith(query) { response, error in
    let collection    = response?.node as? Storefront.Collection
    let productCursor = collection?.products.edges.last?.cursor
}
task.resume()

相应的GraphQL查询看起来像这样

query {
  node(id: "IjoxNDg4MTc3MzEsImxhc3R") {
    ... on Collection {
      products(first: 10, after: "sdWUiOiIxNDg4MTc3M") {
        pageInfo {
          hasNextPage
        }
        edges {
          cursor
          node {
            id
            title
            productType
            description
          }
        }
      }
    }
  }
}

由于我们确切知道我们想获取哪个集合中的产品,我们将使用node接口通过id来查询集合。你可能也注意到,我们正在检索一些额外的字段和对象:pageInfocursor。然后我们可以使用任何产品边界的cursor来获取它之前或之后的更多产品。同样,pageInfo对象提供了关于下一页(以及潜在的上一页)是否可用的附加元数据。

获取产品详情

在我们的应用示例中,我们可能会希望有一个包含图片、变体和描述的详细产品页面。按照传统方式,我们需要进行多个REST调用才能检索所需的所有信息。但使用Buy SDK,我们可以使用单个查询来完成这个任务。

let query = Storefront.buildQuery { $0
    .node(id: productID) { $0
        .onProduct { $0
            .id()
            .title()
            .description()
            .images(first: 10) { $0
                .edges { $0
                    .node { $0
                        .id()
                        .src()
                    }
                }
            }
            .variants(first: 10) { $0
                .edges { $0
                    .node { $0
                        .id()
                        .price()
                        .title()
                        .available()
                    }
                }
            }
        }
    }
}

let task = client.queryGraphWith(query) { response, error in
    let product  = response?.node as? Storefront.Product
    let images   = product?.images.edges.map { $0.node }
    let variants = product?.variants.edges.map { $0.node }
}
task.resume()

相应的GraphQL查询看起来像这样

{
  node(id: "9Qcm9kdWN0LzMzMj") {
    ... on Product {
      id
      title
      description
      images(first: 10) {
        edges {
          node {
            id
            src
          }
        }
      }
      variants(first: 10) {
        edges {
          node {
            id
            price
            title
            available
          }
        }
      }
    }
  }
}

结帐

浏览产品和集合后,客户可能会想要购买一些商品。由于不同应用的要求可能不同,Buy SDK 不提供本地购物车的支持。相反,需由自定义店面来实现该功能。不过,当客户准备好进行购买时,您需要创建一个结帐流程。

几乎每个 mutation 请求都需要一个输入对象。这个对象决定了针对特定资源将突变哪些字段。在这种情况下,我们需要创建一个 Storefront.CheckoutCreateInput

let input = Storefront.CheckoutCreateInput.create(
    lineItems: .value([
        Storefront.CheckoutLineItemInput.create(variantId: GraphQL.ID(rawValue: "mFyaWFu"), quantity: 5),
        Storefront.CheckoutLineItemInput.create(variantId: GraphQL.ID(rawValue: "8vc2hGl"), quantity: 3),
    ])
)

结帐输入对象接受其他参数,如 emailshippingAddress。在我们的示例中,我们直到稍后才能获取到客户的信息,所以在这个突变中不包括它们。给定结帐输入,我们可以执行 checkoutCreate 突变。

let mutation = Storefront.buildMutation { $0
    .checkoutCreate(input: checkout) { $0
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

let task = client.mutateGraphWith(mutation) { result, error in
    guard error == nil else {
        // handle request error
    }

    guard let userError = result?.checkoutCreate?.userErrors else {
        // handle any user error
        return
    }

    let checkoutID = result?.checkoutCreate?.checkout?.id
}
task.resume()

在可能的情况下,始终在突变的有效载荷查询中包含 userErrors 字段。您应该在发起突变请求前始终验证用户输入,但可能会有已验证的用户输入导致客户端和服务器之间的不匹配。在这种情况下,userErrors 包含有关任何无效或缺失字段的 fieldmessage 错误。

由于我们需要在后续更新结帐时添加额外的信息,所以我们只需要在这个突变中的结帐的 id 来保持参考,这样可以跳过 Storefront.Checkout 上的所有其他字段以提高效率和减少带宽。

更新结帐

在创建结帐时,可能无法获取客户信息。Buy SDK 提供更新特定结帐字段(如 emailshippingAddressshippingLine 字段)所需的突变。

请注意,如果您的结帐包含需要运输的行项目,您必须在结帐时提供运输地址和运输行。

要获取更新运输行所需的句柄,您必须首先轮询运输费率。

更新邮箱
let mutation = Storefront.buildMutation { $0
    .checkoutEmailUpdate(checkoutId: id, email: "[email protected]") { $0
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}
更新收货地址
let shippingAddress: Storefront.MailingAddressInput
let mutation = Storefront.buildMutation { $0
    .checkoutShippingAddressUpdate(shippingAddress: shippingAddress, checkoutId: id) {
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}
轮询结账准备

结账可能包含异步操作,可能需要时间完成。如果您想完成结账或确保所有字段都已填充并更新,则需要轮询,直到返回值中的 ready 为真。异步填充的字段包括关税和税费。

所有异步计算完成后,结账将被相应更新。一旦 checkout.ready 标志为真,则进行更新。应在每次更新结账之后检查此标志(如果它为 false,则轮询),以确保没有异步进程正在运行,这可能会影响结账的字段。常见示例包括在更新收货地址或调整结账的行项目后。

let query = Storefront.buildQuery { $0
    .node(id: checkoutID) { $0
        .onCheckout { $0
            .id()
            .ready() // <- Indicates that all fields are up to date after asynchronous operations completed.
            .totalDuties { $0
                .amount()
                .currencyCode()
            }
            .totalTaxV2 { $0
                .amount()
                .currencyCode()
            }
            .totalPriceV2 { $0
                .amount()
                .currencyCode()
            }
            // ...
        }
    }
}

轮询更新和使用响应中返回的更新字段是您应用程序的责任。直到返回值中的 ready == true,需要继续重试查询。Buy SDK 有内置的重试请求支持,因此我们将创建一个重试处理程序并执行查询

let retry = Graph.RetryHandler<Storefront.QueryRoot>(endurance: .finite(10)) { (response, _) -> Bool in
    (response?.node as? Storefront.Checkout)?.ready ?? false == false
}

let task = self.client.queryGraphWith(query, retryHandler: retry) { response, error in
    let updatedCheckout = response?.node as? Storefront.Checkout
}
task.resume()

只有当 checkout.ready == true 或重试计数达到 10 时,才会调用完成。虽然您可以指定重试处理程序 endurance 属性的 .infinite,但我们强烈建议您设置一个有限限制。

查看运费

可用的运费率针对每个结账页面特定,因为运输物品的成本取决于物品的件数、重量和其他属性。运费率还要求结账拥有有效的 shippingAddress,这可以通过在更新结账信息中找到的步骤进行更新。可用的运费率是 Storefront.Checkout 字段中的一个字段,因此给定一个 checkoutID(我们之前保留了一个引用),我们可以查询运费

let query = Storefront.buildQuery { $0
    .node(id: checkoutID) { $0
        .onCheckout { $0
            .id()
            .availableShippingRates { $0
                .ready()
                .shippingRates { $0
                    .handle()
                    .price()
                    .title()
                }
            }
        }
    }
}

上述查询在服务器上启动了一个异步任务,从多个运输服务提供商抓取运费率。尽管请求可能会立即返回(不考虑网络延迟),但这并不意味着运费率列表已经完整。这在上面的查询中的 ready 字段中有体现。继续重试此查询直到 ready == true 是您应用程序的责任。Buy SDK 有内置重试请求支持,因此我们将创建一个重试处理程序并执行查询

let retry = Graph.RetryHandler<Storefront.QueryRoot>(endurance: .finite(10)) { (response, error) -> Bool in
    return (response?.node as? Storefront.Checkout)?.availableShippingRates?.ready ?? false == false
}

let task = self.client.queryGraphWith(query, retryHandler: retry) { response, error in
    let checkout      = (response?.node as? Storefront.Checkout)
    let shippingRates = checkout.availableShippingRates?.shippingRates
}
task.resume()

只有当 availableShippingRates.ready == true 或重试计数达到10时,才会调用完成。尽管您可以指定重试处理程序的 endurance 属性为 .infinite,但我们强烈建议您设置一个有限的限制。

更新运费项
let mutation = Storefront.buildMutation { $0
    .checkoutShippingLineUpdate(checkoutId: id, shippingRateHandle: shippingRate.handle) { $0
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

完成结账

在填写所有必填字段并客户准备好付款后,您可以有三种方式来完成结账和处理付款

Web checkout

完成结账最简单的方法是通过将客户重定向到一个网视图,在那里他们会看到与他们在网络上熟悉的相同的流程。《Storefront.Checkout》资源提供了《webUrl》,您可以使用它来显示网页视图。我们强烈建议使用《SFSafariViewController》而不是《WKWebView》或其他替代方案。

注意:尽管使用网页结账是最简单的方法,但在观察结账状态时存在一些困难。由于网页视图不提供对各种结账状态的回调,您仍然需要轮询结账完成

信用卡结账

原生信用卡结账为三种选择中提供最传统用户界面,但它需要最大努力来实现。您将需要实现收集客户姓名、电子邮件、地址、支付信息和其他完成结账所必需的字段的UI。

假设您的自定义商店前端有所需的所有信息,完成信用卡结账的第一步是将提供的信用卡存档并交换为完成支付将使用的支付令牌。想了解更多,请参阅存档信用卡的说明。

在获取信用卡存档令牌后,我们接下来可以通过创建《CreditCardPaymentInput》并执行突变查询来完成结账。

// let paySession: PaySession
// let payAuthorization: PayAuthorization
// let moneyInput: MoneyInput

let payment = Storefront.CreditCardPaymentInputV2.create(
    paymentAmount:  moneyInput,
    idempotencyKey: paySession.identifier,
    billingAddress: self.mailingAddressInputFrom(payAuthorization.billingAddress,
    vaultId:        token
)

let mutation = Storefront.buildMutation { $0
    .checkoutCompleteWithCreditCardV2(checkoutId: checkoutID, payment: payment) { $0
        .payment { $0
            .id()
            .ready()
        }
        .checkout { $0
            .id()
            .ready()
        }
        .checkoutUserErrors { $0
            .code()
            .field()
            .message()
        }
    }
}

let task = client.mutateGraphWith(mutation) { result, error in
    guard error == nil else {
        // handle request error
    }

    guard let userError = result?.checkoutCompleteWithCreditCardV2?.checkoutUserErrors else {
        // handle any user error
        return
    }

    let checkoutReady = result?.checkoutCompleteWithCreditCardV2?.checkout.ready ?? false
    let paymentReady  = result?.checkoutCompleteWithCreditCardV2?.payment?.ready ?? false

    // checkoutReady == false
    // paymentReady == false
}
task.resume()

3D Secure结账

要在您的工作流程中实施3D secure,请参阅API帮助文档

Apple Pay 订单支付

重要:在完成 Apple Pay 令牌的订单支付之前,您应该确保您的订单支付已更新至最新的发货地址,该地址由 paySession(_:didAuthorizePayment:checkout:completeTransaction:) 代理回调提供。如果您之前在订单支付中设置了一个部分地址以获取运输费用,则 当前的订单支付不会成功完成。您必须将订单支付更新为包括地址行和完整的邮编或邮政编码的完整地址。

Buy SDK 通过提供的 Pay.framework 实现了  Pay 集成的简单性。要了解如何设置和使用 PaySession 来获取付款令牌,请参考  Pay 部分。获取付款令牌后,我们可以完成订单支付

// let paySession: PaySession
// let payCheckout: PayCheckout
// let payAuthorization: PayAuthorization

let payment = Storefront.TokenizedPaymentInput.create(
    amount:         payCheckout.paymentDue,
    idempotencyKey: paySession.identifier,
    billingAddress: self.mailingAddressInputFrom(payAuthorization.billingAddress), // <- perform the conversion
    type:           "apple_pay",
    paymentData:    payAuthorization.token
)

let mutation = Storefront.buildMutation { $0
    .checkoutCompleteWithTokenizedPayment(checkoutId: checkoutID, payment: payment) { $0
        .payment { $0
            .id()
            .ready()
        }
        .checkout { $0
            .id()
            .ready()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

let task = client.mutateGraphWith(mutation) { result, error in
    guard error == nil else {
        // handle request error
    }

    guard let userError = result?.checkoutCompleteWithTokenizedPayment?.userErrors else {
        // handle any user error
        return
    }

    let checkoutReady = result?.checkoutCompleteWithTokenizedPayment?.checkout.ready ?? false
    let paymentReady  = result?.checkoutCompleteWithTokenizedPayment?.payment?.ready ?? false

    // checkoutReady == false
    // paymentReady == false
}
task.resume()

检查订单完成状态

在成功 checkoutCompleteWith... 变异之后,订单支付流程开始。此流程通常较短,但不是立即的。因此需要轮询以获取更新为 ready 状态的订单,其中包含 Storefront.Order

let retry = Graph.RetryHandler<Storefront.QueryRoot>(endurance: .finite(30)) { (response, error) -> Bool in
    return (response?.node as? Storefront.Checkout)?.order == nil
}

let query = Storefront.buildQuery { $0
    .node(id: checkoutID) { $0
        .onCheckout { $0
            .order { $0
                .id()
                .createdAt()
                .orderNumber()
                .totalPrice()
            }
        }
    }
}

let task  = self.client.queryGraphWith(query, retryHandler: retry) { response, error in
    let checkout = (response?.node as? Storefront.Checkout)
    let orderID  = checkout?.order?.id
}
task.resume()

又例如当 轮询获取可用的运输费用 时,我们需要创建一个 RetryHandler 来提供一个条件,以便在满足该条件时重试请求。在这种情况下,我们断言 Storefront.Order 为空,并且如果它为空,我们将继续重试请求。

处理错误

Graph.Client 可以返回非空的 Graph.QueryError错误和结果是相互独立的。 同时拥有错误和结果是有效的。然而,在这个实例中,错误 case总是.invalidQuery(let reasons)。您应该始终评估错误,确保您的查询操作有效,然后再评估结果。

let task = self.client.queryGraphWith(query) { result, error in

    if let error = error, case .invalidQuery(let reasons) = error {
        reasons.forEach {
            print("Error on \($0.line):\($0.column) - \($0.message)")
        }
    }

    if let result = result {
        // Do something with the result
    } else {
        // Handle any other errors
    }
}
task.resume()

重要: Graph.QueryError 不包含用户友好的信息。通常,它描述了失败的技术原因,不应显示给最终用户。处理错误对调试最有用。

客户账户

使用 Buy SDK,您可以构建定制的店面,让您的客户创建账户、浏览之前完成的订单以及管理他们的信息。由于大多数与客户相关的操作都会修改服务器上的状态,因此它们使用各种 mutation 请求执行。让我们看看一些示例。

创建客户

在客户可以登录之前,他们必须首先创建账户。在您的应用程序中,您可以提供一个运行以下 mutation 请求的注册表单。在这个例子中,该变体的 input 是创建您商店账户的一些基本客户信息。

let input = Storefront.CustomerCreateInput.create(
    email:            .value("[email protected]"),
    password:         .value("123456"),
    firstName:        .value("John"),
    lastName:         .value("Smith"),
    acceptsMarketing: .value(true)
)

let mutation = Storefront.buildMutation { $0
    .customerCreate(input: input) { $0
        .customer { $0
            .id()
            .email()
            .firstName()
            .lastName()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

请注意,此变异返回一个 Storefront.Customer 对象,NOT 访问令牌。在变异成功之后,客户仍需要使用他们的凭据 登录

客户登录

任何拥有账户的客户都可以登录您的店铺。所有登录操作都是以 mutation 请求的形式进行的,客户凭据交换为访问令牌。您可以使用 customerAccessTokenCreate mutation 来登录客户。请注意,返回的访问令牌最终会过期。返回负载的 expiresAt 属性提供了过期 Date

let input = Storefront.CustomerAccessTokenCreateInput.create(
    email:    "[email protected]",
    password: "123456"
)

let mutation = Storefront.buildMutation { $0
    .customerAccessTokenCreate(input: input) { $0
        .customerAccessToken { $0
            .accessToken()
            .expiresAt()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

可选地,您可以定期使用 customerAccessTokenRenew mutation 来刷新自定义访问令牌。

重要:确保安全存储客户访问令牌是你的责任。我们建议使用 Keychain 和存储安全数据的最佳实践。

密码重置

偶尔,客户可能会忘记他们的账户密码。SDK 提供了一种方法,允许您的应用程序重置客户的密码。简单的实现可以简单地调用 recover mutation,此时客户将收到一封电子邮件,说明如何在网页浏览器中重置他们的密码。

以下 mutation 以客户电子邮件为参数,如果输入有问题,则返回 payload 中的 userErrors

let mutation = Storefront.buildMutation { $0
    .customerRecover(email: "[email protected]") { $0
        .userErrors { $0
            .field()
            .message()
        }
    }
}

创建、更新和删除地址

您可以使用适当的 mutation 在客户 behalf 上创建、更新和删除地址。请注意,这些 mutations 需要客户身份验证。每个查询都需要一个客户访问令牌作为参数来执行 mutation。

以下示例显示了一个创建地址的 mutation 示例:

let input = Storefront.MailingAddressInput.create(
    address1:  .value("80 Spadina Ave."),
    address2:  .value("Suite 400"),
    city:      .value("Toronto"),
    country:   .value("Canada"),
    firstName: .value("John"),
    lastName:  .value("Smith"),
    phone:     .value("1-123-456-7890"),
    province:  .value("ON"),
    zip:       .value("M5V 2J4")
)

let mutation = Storefront.buildMutation { $0
    .customerAddressCreate(customerAccessToken: token, address: input) { $0
        .customerAddress { $0
            .id()
            .address1()
            .address2()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

客户信息

迄今为止,我们与客户信息的交互是通过 mutation 请求完成的。在某个时刻,我们还需要展示客户的信息。我们可以使用客户的 query 操作来实现。

与地址突变一样,客户 query 操作需要身份验证,并且执行时需要有效的访问令牌。以下示例演示了如何获取一些基本的客户信息。

let query = Storefront.buildQuery { $0
    .customer(customerAccessToken: token) { $0
        .id()
        .firstName()
        .lastName()
        .email()
    }
}

客户地址

你可以获取与客户账户关联的地址。

let query = Storefront.buildQuery { $0
    .customer(customerAccessToken: token) { $0
        .addresses(first: 10) { $0
            .edges { $0
                .node { $0
                    .address1()
                    .address2()
                    .city()
                    .province()
                    .country()
                }
            }
        }
    }
}

客户订单

你也可以获取客户的订单历史。

let query = Storefront.buildQuery { $0
    .customer(customerAccessToken: token) { $0
        .orders(first: 10) { $0
            .edges { $0
                .node { $0
                    .id()
                    .orderNumber()
                    .totalPrice()
                }
            }
        }
    }
}

客户更新

输入对象,如 Storefront.MailingAddressInput,使用 Input(其中 T 是值的类型)来表示可选字段,并将 nil 值与 undefined 值区分开来(例如,phone: Input)。

以下示例使用 Storefront.CustomerUpdateInput 来演示如何更新客户的电话号码。

let input = Storefront.CustomerUpdateInput(
    phone: .value("+16471234567")
)

在此示例中,您创建一个输入对象,设置 phone 字段为要更新的新电话号码。请注意,您需要传递 Input.value() 而不是包含电话号码的简单字符串。

Storefront.CustomerUpdateInput 对象还包括除 phone 字段之外的其他字段。如果您未指定这些字段的值,则它们的默认值为 .undefined。这意味着这些字段不会被序列化,并将被完全省略。结果看起来就像这样。

mutation {
  customerUpdate(
    customer: { phone: "+16471234567" }
    customerAccessToken: "..."
  ) {
    customer {
      phone
    }
  }
}

此方法对于设置新电话号码或更新现有电话号码至新值非常有效。但如果客户想要完全删除电话号码怎么办?将电话号码留空或发送空字符串在语义上是不同的,并不会达到预期效果。前者表示我们没有定义值,而后者则会返回无效电话号码错误。这就是为什么Input<T>特别有用的地方。您可以通过指定nil值来使用它来表示删除电话号码的意图。

let input = Storefront.CustomerUpdateInput(
    phone: .value(nil)
)

结果是更新客户电话号码为null的变更。

mutation {
  customerUpdate(customer: { phone: null }, customerAccessToken: "...") {
    customer {
      phone
    }
  }
}

示例应用

有关开始使用的信息,请查看示例iOS应用。它涵盖了SDK最常用的用例以及如何与其集成。将示例应用作为模板、起点或按需选择组件的地方。有关更多详细信息,请参阅应用自述文件。

贡献

我们欢迎贡献。请按照我们贡献指南中的步骤操作。

帮助

有关Mobile Buy SDK的帮助,请参阅iOS Buy SDK文档或在Shopify APIs & SDKs部分中发布问题至我们的论坛

许可

移动购买SDK在MIT许可下提供。