Mobile Buy SDK
Mobile Buy SDK 可以轻松地在您的移动应用程序中创建自定义店面,用户可以使用 Apple Pay 或信用卡购买产品。SDK 通过 GraphQL 连接到 Shopify 平台,并支持广泛的本地店面体验。
目录
↑
文档您可以通过运行Documentation
方案生成完整的HTML和.docset
文档。然后,您可以使用文档浏览器,例如Dash来访问.docset
工件,或直接在Docs/Buy
和Docs/Pay
目录中浏览HTML。
此文档使用Jazzy生成。
↑
安装
↑
Swift 包管理器 这是将SDK与您的应用程序集成的推荐方法。您可以按照Apple的指南,在应用中添加包依赖项来进行详细操作。
🔼
动态框架安装- 通过运行以下命令将
Buy
作为git子模块添加:
git submodule add [email protected]:Shopify/mobile-buy-sdk-ios.git
- 确保已经通过运行以下命令更新了
Buy SDK
的所有子模块:
git submodule update --init --recursive
- 将
Buy.xcodeproj
拖入您的应用程序项目中。 - 将
Buy.framework
作为依赖项添加- 导航到构建阶段 > 目标依赖项。
- 添加
Buy.framework
。
- 链接
Buy.framework
- 导航到构建阶段 > 与库链接二进制。
- 添加
Buy.framework
。
- 确保框架已复制到应用程序包中
- 导航到构建阶段 > 新建复制文件阶段。
- 从目标下拉列表中选择框架。
- 添加
Buy.framework
。
- 使用
import Buy
将它们导入到项目文件中。
查看Storefront
示例应用,了解如何将Buy
目标添加为依赖项的示例。
🔼
Carthage- 将以下行添加到您的Cartfile中:
github "Shopify/mobile-buy-sdk-ios"
- 运行
carthage update
。 - 按照链接Carthage生成的动态框架的步骤进行操作。
- 导入SDK模块
import Buy
🔼
CocoaPods- 将以下行添加到您的Podfile中:
pod "Mobile-Buy-SDK"
- 运行
pod install
。 - 导入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.ClientGraph.Client
是在URLSession
之上构建的一个网络层,用于执行查询
和提交
请求。它还简化了轮询和重试请求。要开始使用Graph.Client
,您需要以下内容
- 您的商店的
.myshopify.com
域名 - 您的API密钥,可以在您的商店管理页面找到
- 一个
URLSession
(可选),如果要自定义用于网络请求的配置或有意愿将现有的URLSession
与Graph.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风格的PUT
、POST
或DELETE
请求相当。变更通常伴随着代表要更新值的输入和用于获取已更新资源字段的查询。您可以将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变更的信息。
↑
重试queryGraphWith
和 mutateGraphWith
都接受一个可选的 RetryHandler<R: GraphQL.AbstractResponse>
。该对象包含重试状态和配置参数,用于确定 Client
如何重试后续请求(例如在设置延迟或重试次数后)。默认情况下,retryHandler
为 nil,不会提供任何重试行为。要启用重试或轮询,需要创建一个带有条件的处理器。如果 handler.condition
和 handler.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
}
重试处理器是通用的,可以同等良好地处理 query
和 mutation
请求。
⤴
缓存 网络查询和变更既可能很低效,也可能代价很高。对于不太经常改变的资源,您可能希望使用缓存来帮助减少带宽和延迟。由于 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
,它表示请求的当前错误状态。请注意,error
和 response
并非互相排斥。存在错误和响应都是完全有效的。错误的存在可以代表网络错误(例如网络错误或无效的 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.Client
,Card.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, didRequestShippingRatesFor address: PayPostalAddress, checkout: PayCheckout, provide: @escaping (PayCheckout?, [PayShippingRate]) -> Void) {
self.updateCheckoutShippingAddress(id: checkout.id, with: address) { updatedCheckout in
if let updatedCheckout = updatedCheckout {
self.fetchShippingRates(for: address) { shippingRates in
if let shippingRates = shippingRates {
/* Be sure to provide an up-to-date checkout that contains the
* shipping address that was used to fetch the shipping rates.
*/
provide(updatedCheckout, shippingRates)
} else {
/* By providing a nil checkout we inform the PaySession that
* we failed to obtain shipping rates with the provided address. An
* "invalid shipping address" error will be displayed to the customer.
*/
provide(nil, [])
}
}
} else {
/* By providing a nil checkout we inform the PaySession that
* we failed to obtain shipping rates with the provided address. An
* "invalid shipping address" error will be displayed to the customer.
*/
provide(nil, [])
}
}
}
当客户在 Pay模态中选择送货联系人时调用。提供的PayPostalAddress
是一个部分地址,排除了街道地址以提高安全性。实际上这是由PassKit
而非Pay
框架强制实施的。然而,PayPostalAddress
中的信息足以从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之上,它继承了edges
和nodes
的概念。
更多关于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
来查询集合。你可能也注意到,我们正在检索一些额外的字段和对象:pageInfo
和cursor
。然后我们可以使用任何产品边界的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),
])
)
结帐输入对象接受其他参数,如 email
和 shippingAddress
。在我们的示例中,我们直到稍后才能获取到客户的信息,所以在这个突变中不包括它们。给定结帐输入,我们可以执行 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
包含有关任何无效或缺失字段的 field
和 message
错误。
由于我们需要在后续更新结帐时添加额外的信息,所以我们只需要在这个突变中的结帐的 id
来保持参考,这样可以跳过 Storefront.Checkout
上的所有其他字段以提高效率和减少带宽。
⤴
更新结帐在创建结帐时,可能无法获取客户信息。Buy SDK 提供更新特定结帐字段(如 email
、shippingAddress
和 shippingLine
字段)所需的突变。
请注意,如果您的结帐包含需要运输的行项目,您必须在结帐时提供运输地址和运输行。
要获取更新运输行所需的句柄,您必须首先轮询运输费率。
⤴
更新邮箱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许可下提供。