Shopify’s Mobile Buy SDK makes it simple to sell physical products inside your mobile app. With a few lines of code, you can connect your app with the Shopify platform and let your users buy your products using Apple Pay or their credit card.
The Mobile Buy SDK makes it easy to create custom storefronts in your mobile app, where users can buy products using Apple Pay or their credit card. The SDK connects to the Shopify platform using GraphQL, and supports a wide range of native storefront experiences.
You can generate a complete HTML and
.docsetdocumentation by running the
Documentationscheme. You can then use a documentation browser like Dash to access the
.docsetartifact, or browse the HTML directly in the
Docs/Buyand
Docs/Paydirectories.
The documentation is generated using Jazzy.
This is the recommended approach of integration the SDK with your app. You can follow Apple's guide for adding a package dependency to your app for a thorough walkthrough.
Buyas a git submodule by running:
git submodule add [email protected]:Shopify/mobile-buy-sdk-ios.git
Buy SDKhave also been updated by running:
git submodule update --init --recursive
Buy.xcodeprojinto your application project.
Buy.framework.
Buy.framework:
Buy.framework.
Buy.framework.
import Buy.
See the Storefront sample app for an example of how to add the
Buytarget a dependency.
ruby github "Shopify/mobile-buy-sdk-ios"
carthage update.
swift import Buy
ruby pod "Mobile-Buy-SDK"
pod install.
swift import MobileBuySDK
Note: If you've forked this repo and are attempting to install from your own git destination, commit, or branch, be sure to include "submodules: true" in the line of your Podfile
The Buy SDK is built on GraphQL. The SDK handles all the query generation and response parsing, exposing only typed models and compile-time checked query structures. It doesn't require you to write stringed queries, or parse JSON responses.
You don't need to be an expert in GraphQL to start using it with the Buy SDK (but it helps if you've used it before). The sections below provide a brief introduction to this system, and some examples of how you can use it to build secure custom storefronts.
The previous version of the Mobile Buy SDK (version 2.0) is based on a REST API. With version 3.0, Shopify is migrating the SDK from REST to GraphQL.
Unfortunately, the specifics of generation GraphQL models make it almost impossible to create a migration path from v2.0 to v3.0 (domains models are not backwards compatible). However, the main concepts are the same across the two versions, such as collections, products, checkouts, and orders.
The Buy SDK is built on a hierarchy of generated classes that construct and parse GraphQL queries and responses. These classes are generated manually by running a custom Ruby script that relies on the GraphQL Swift Generation library. Most of the generation functionality and supporting classes live inside the library. It works by downloading the GraphQL schema, generating Swift class hierarchy, and saving the generated files to the specified folder path. In addition, it provides overrides for custom GraphQL scalar types like
DateTime.
All generated request models are derived from the
GraphQL.AbstractQuerytype. Although this abstract type contains enough functionality to build a query, you should never use it directly. Instead, rely on the typed methods provided in the generated subclasses.
The following example shows a sample query for a shop's name:
let query = Storefront.buildQuery { $0 .shop { $0 .name() } }
Never use the abstract class directly:
// Never do thislet shopQuery = GraphQL.AbstractQuery() shopQuery.addField(field: "name")
let query = GraphQL.AbstractQuery() query.addField(field: "shop", subfields: shopQuery)
Both of the above queries produce identical GraphQL queries (see below), but the former approach provides auto-completion and compile-time validation against the GraphQL schema. It will surface an error if a requested field doesn't exist, isn't the correct type, or is deprecated. You also might have noticed that the former approach resembles the GraphQL query language structure (this is intentional). The query is both easier to write and much more legible.
query { shop { name } }
All generated response models are derived from the
GraphQL.AbstractResponsetype. This abstract type provides a similar key-value type interface to a
Dictionaryfor accessing field values in GraphQL responses. Just like
GraphQL.AbstractQuery, you should never use these accessors directly, and instead rely on typed, derived properties in generated subclasses.
The following example builds on the earlier example of accessing the result of a shop name query:
// response: Storefront.QueryRootlet name: String = response.shop.name
Never use the abstract class directly:
// Never do thislet response: GraphQL.AbstractResponse
let shop = response.field("shop") as! GraphQL.AbstractResponse let name = shop.field("name") as! String
Again, both of the approaches produce the same result, but the former case is preferred: it requires no casting since it already knows about the expected type.
Nodeprotocol ⤴
The GraphQL schema defines a
Nodeinterface that declares an
idfield on any conforming type. This makes it convenient to query for any object in the schema given only its
id. The concept is carried across to the Buy SDK as well, but requires a cast to the correct type. You need to make sure that the
Nodetype is of the correct type, otherwise casting to an incorrect type will return a runtime exception.
Given this query:
let id = GraphQL.ID(rawValue: "NkZmFzZGZhc") let query = Storefront.buildQuery { $0 .node(id: id) { $0 .onOrder { $0 .id() .createdAt() } } }
The
Storefront.Orderrequires a cast:
// response: Storefront.QueryRootlet order = response.node as! Storefront.Order
Aliases are useful when a single query requests multiple fields with the same names at the same nesting level, since GraphQL allows only unique field names. Multiple nodes can be queried by using a unique alias for each one:
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 } } }
Accessing the aliased nodes is similar to a plain node:
// response: Storefront.QueryRootlet collection = response.aliasedNode(aliasSuffix: "collection") as! Storefront.Collection let product = response.aliasedNode(aliasSuffix: "product") as! Storefront.Product
Learn more about GraphQL aliases.
The
Graph.Clientis a network layer built on top of
URLSessionthat executes
queryand
mutationrequests. It also simplifies polling and retrying requests. To get started with
Graph.Client, you need the following:
.myshopify.comdomain
URLSession(optional), if you want to customize the configuration used for network requests or share your existing
URLSessionwith the
Graph.Client
let client = Graph.Client( shopDomain: "shoes.myshopify.com", apiKey: "dGhpcyBpcyBhIHByaXZhdGUgYXBpIGtleQ" )
If your store supports multiple languages, then the Storefront API can return translated resource types and fields. Learn more about translating content.
// Initializing a client to return translated content let client = Graph.Client( shopDomain: "shoes.myshopify.com", apiKey: "dGhpcyBpcyBhIHByaXZhdGUgYXBpIGtleQ", locale: Locale.current )
GraphQL specifies two types of operations: queries and mutations. The
Clientexposes these as two type-safe operations, although it also offers some conveniences for retrying and polling in each one.
Semantically, a GraphQL
queryoperation is equivalent to a
GETRESTful call. It guarantees that no resources will be mutated on the server. With
Graph.Client, you can perform a query operation using:
public func queryGraphWith(_ query: Storefront.QueryRootQuery, retryHandler: RetryHandler? = default, completionHandler: QueryCompletion) -> Task
The following example shows how you can query for a shop's name:
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()
Learn more about GraphQL queries.
Semantically a GraphQL
mutationoperation is equivalent to a
PUT,
POSTor
DELETERESTful call. A mutation is almost always accompanied by an input that represents values to be updated and a query to fetch fields of the updated resource. You can think of a
mutationas a two-step operation where the resource is first modified, and then queried using the provided
query. The second half of the operation is identical to a regular
queryrequest.
With
Graph.Clientyou can perform a mutation operation using:
public func mutateGraphWith(_ mutation: Storefront.MutationQuery, retryHandler: RetryHandler? = default, completionHandler: MutationCompletion) -> Task
The following example shows how you can reset a customer's password using a recovery token:
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()
A mutation will often rely on some kind of user input. Although you should always validate user input before posting a mutation, there are never guarantees when it comes to dynamic data. For this reason, you should always request the
userErrorsfield on mutations (where available) to provide useful feedback in your UI regarding any issues that were encountered in the mutation query. These errors can include anything from
Invalid email addressto
Password is too short.
Learn more about GraphQL mutations.
Both
queryGraphWithand
mutateGraphWithaccept an optional
RetryHandler. This object encapsulates the retry state and customization parameters for how the
Clientwill retry subsequent requests (such as after a set delay, or a number of retries). By default, the
retryHandleris nil and no retry behavior will be provided. To enable retry or polling, create a handler with a condition. If the
handler.conditionand
handler.canRetryevaluate to
true, then the
Clientwill continue executing the request:
let handler = Graph.RetryHandler() { (query, error) -> Bool in if myCondition { return true // will retry } return false // will complete the request, either succeed or fail }
The retry handler is generic, and can handle both
queryand
mutationrequests equally well.
Network queries and mutations can be both slow and expensive. For resources that change infrequently, you might want to use caching to help reduce both bandwidth and latency. Since GraphQL relies on
POSTrequests, we can't easily take advantage of the HTTP caching that's available in
URLSession. For this reason, the
Graph.Clientis equipped with an opt-in caching layer that can be enabled client-wide or on a per-request basis.
IMPORTANT: Caching is provided only for
queryoperations. It isn't available for
mutationoperations or for any other requests that provide a
retryHandler.
There are four available cache policies:
.cacheOnly- Fetch a response from the cache only, ignoring the network. If the cached response doesn't exist, then return an error.
.networkOnly- Fetch a response from the network only, ignoring any cached responses.
.cacheFirst(expireIn: Int)- Fetch a response from the cache first. If the response doesn't exist or is older than
expireIn, then fetch a response from the network
.networkFirst(expireIn: Int)- Fetch a response from the network first. If the network fails and the cached response isn't older than
expireIn, then return cached data instead.
You can enable client-wide caching by providing a default
cachePolicyfor any instance of
Graph.Client. This sets all
queryoperations to use your default cache policy, unless you specify an alternate policy for an individual request.
In this example, we set the client's
cachePolicyproperty to
cacheFirst:
let client = Graph.Client(shopDomain: "...", apiKey: "...") client.cachePolicy = .cacheFirst
Now, all calls to
queryGraphWithwill yield a task with a
.cacheFirstcache policy.
If you want to override a client-wide cache policy for an individual request, then specify an alternate cache policy as a parameter of
queryGraphWith:
let task = client.queryGraphWith(query, cachePolicy: .networkFirst(expireIn: 20)) { query, error in // ... }
In this example, the
taskcache policy changes to
.networkFirst(expireIn: 20), which means that the cached response will be valid for 20 seconds from the time the response is received.
The completion for either a
queryor
mutationrequest will always contain an optional
Graph.QueryErrorthat represents the current error state of the request. It's important to note that
errorand
responseare NOT mutually exclusive. It is perfectly valid to have a non-nil error and response. The presence of an error can represent both a network error (such as a network error, or invalid JSON) or a GraphQL error (such as invalid query syntax, or a missing parameter). The
Graph.QueryErroris an
enum, so checking the type of error is trivial:
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()
If the error is of type
.invalidQuery, then an array of
Reasonobjects is returned. These will provide more in-depth information about the query error. Keep in mind that these errors are not meant to be displayed to the end-user. They are for debugging purposes only.
The following example shows a GraphQL error response for an invalid query:
{ "errors": [ { "message": "Field 'Shop' doesn't exist on type 'QueryRoot'", "locations": [ { "line": 2, "column": 90 } ], "fields": [ "query CollectionsWithProducts", "Shop" ] } ] }
Learn more about GraphQL errors.
Some
Storefrontmodels accept search terms via the
queryparameter. For example, you can provide a
queryto search for collections that contain a specific search term in any of their fields.
The following example shows how you can find collections that contain the word "shoes":
let query = Storefront.buildQuery { $0 .shop { $0 .collections(first: 10, query: "shoes") { $0 .edges { $0 .node { $0 .id() .title() .description() } } } } }
In the example above, the query is
shoes. This will match collections that contain "shoes" in the description, title, and other fields. This is the simplest form of query. It provides fuzzy matching of search terms on all fields of a collection.
As an alternative to object-wide fuzzy matches, you can also specify individual fields to include in your search. For example, if you want to match collections of particular type, you can do so by specifying a field directly:
.collections(first: 10, query: "collection_type:runners") { $0 ... }
The format for specifying fields and search parameters is the following:
field:search_term. Note that it's critical that there be no space between the
:and the
search_term. Fields that support search are documented in the generated interfaces of the Buy SDK.
IMPORTANT: If you specify a field in a search (as in the example above), then the
search_termwill be an exact match instead of a fuzzy match. For example, based on the query above, a collection with the type
blue_runnerswill not match the query for
runners.
Each search field can also be negated. Building on the example above, if you want to match all collections that were not of the type
runners, then you can append a
-to the relevant field:
.collections(first: 10, query: "-collection_type:runners") { $0 ... }
In addition to single field searches, you can build more complex searches using boolean operators. They very much like ordinary SQL operators.
The following example shows how you can search for products that are tagged with
blueand that are of type
sneaker:
.products(first: 10, query: "tag:blue AND product_type:sneaker") { $0 ... }
You can also group search terms:
.products(first: 10, query: "(tag:blue AND product_type:sneaker) OR tag:red") { $0 ... }
The search syntax also allows for comparing values that aren't exact matches. For example, you might want to get products that were updated only after a certain a date. You can do that as well:
.products(first: 10, query: "updated_at:>\"2017-05-29T00:00:00Z\"") { $0 ... }
The query above will return products that have been updated after midnight on May 29, 2017. Note how the date is enclosed by another pair of escaped quotations. You can also use this technique for multiple words or sentences.
The SDK supports the following comparison operators:
:equal to
:<less than
:>greater than
:<=less than or equal to
:>=greater than or equal to
IMPORTANT:
:=is not a valid operator.
There is one special operator that can be used for checking
nilor empty values.
The following example shows how you can find products that don't have any tags. You can do so using the
*operator and negating the field:
.products(first: 10, query: "-tag:*") { $0 ... }
The Buy SDK support native checkout via GraphQL, which lets you complete a checkout with a credit card. However, it doesn't accept credit card numbers directly. Instead, you need to vault the credit cards via the standalone, PCI-compliant web service. The Buy SDK makes it easy to do this using
Card.Client.
Like
Graph.Client, the
Card.Clientmanages your interactions with the card server that provides opaque credit card tokens. The tokens are used to complete checkouts. After collecting the user's credit card information in a secure manner, create a credit card representation and submit a vault request:
// let settings: Storefront.PaymentSettings // let cardClient: Card.Clientlet 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()
IMPORTANT: The credit card vaulting service does not provide any validation for submitted credit cards. As a result, submitting invalid credit card numbers or even missing fields will always yield a vault
token. Any errors related to invalid credit card information will be surfaced only when the provided
tokenis used to complete a checkout.
Support for Pay is provided by the
Payframework. It is compiled and tested separately from the
BuySDK and offers a simpler interface for supporting Pay in your application. It is designed to take the guess work out of using partial GraphQL models with
PKPaymentAuthorizationController.
When the customer is ready to pay for products in your application with Pay, the
PaySessionencapsulates all the states necessary to complete the checkout process:
merchantID
To present the Pay modal and begin the checkout process, you need:
Storefront.Checkout
queryon
Storefront.Shop
merchantID
After all the prerequisites have been met, you can initialize a
PaySessionand start the payment authorization process:
self.paySession = PaySession( checkout: payCheckout, currency: payCurrency, merchantID: "com.merchant.identifier" )self.paySession.delegate = self self.paySession.authorize()
After calling
authorize(), the session will create a
PKPaymentAuthorizationControlleron your behalf and present it to the customer. By providing a
delegate, you'll be notified when the customer changes shipping address, selects a shipping rate, and authorizes the payment using TouchID or passcode. It is critical to correctly handle each one of these events by updating the
Storefront.Checkoutwith appropriate mutations. This keeps the checkout state on the server up-to-date.
Let's take a look at each one:
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, []) } }
}
Invoked when the customer has selected a shipping contact in the Pay modal. The provided
PayPostalAddressis a partial address that excludes the street address for added security. This is actually enforced by
PassKitand not the
Payframework. Nevertheless, information contained in
PayPostalAddressis sufficient to obtain an array of available shipping rates from
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) } }
}
Invoked every time the customer selects a different shipping and the first time shipping rates are updated as a result of the previous
delegatecallback.
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) }
}
Invoked when the customer authorizes the payment. At this point, the
delegatewill receive the encrypted
tokenand other associated information that you'll need for the final
completeCheckoutmutation to complete the purchase. The state of the checkout on the server must be up-to-date before invoking the final checkout completion mutation. Make sure that all in-flight update mutations are finished before completing checkout.
func paySessionDidFinish(_ paySession: PaySession) { // Do something after the Pay modal is dismissed }
Invoked when the Pay modal is dismissed, regardless of whether the payment authorization was successful or not.
Getting started with any SDK can be confusing. The purpose of this section is to explore all areas of the Buy SDK that might be necessary to build a custom storefront on iOS and provide a solid starting point for your own implementation.
In this section we're going to assume that you've set up a client somewhere in your source code. Although it's possible to have multiple instances of
Graph.Client, reusing a single instance offers many behind-the-scenes performance improvements:
let client: Graph.Client
Before you display products to the user, you typically need to obtain various metadata about your shop. This can be anything from a currency code to your shop's name:
let query = Storefront.buildQuery { $0 .shop { $0 .name() .currencyCode() .refundPolicy { $0 .title() .url() } } }let task = client.queryGraphWith(query) { response, error in let name = response?.shop.name let currencyCode = response?.shop.currencyCode let moneyFormat = response?.shop.moneyFormat } task.resume()
The corresponding GraphQL query looks like this:
query { shop { name currencyCode refundPolicy { title url } } }
In our sample custom storefront, we want to display a collection with a preview of several products. With a conventional RESTful service, this would require one network call for collections and another network call for each collection in that array. This is often referred to as the
n + 1problem.
The Buy SDK is built on GraphQL, which solves the
n + 1request problem. In the following example, a single query retrieves 10 collection and 10 products for each collection with just one network request:
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()
The corresponding GraphQL query looks like this:
{ shop { collections(first: 10) { edges { node { id title products(first: 10) { edges { node { id title productType description } } } } } } } }
Since it only retrieves a small subset of properties for each resource, this GraphQL call is also much more bandwidth-efficient than it would be to fetch 100 complete resources via conventional REST.
But what if you need to get more than 10 products in each collection?
Although it might be convenient to assume that a single network request will suffice for loading all collections and products, that might be naive. The best practice is to paginate results. Since the Buy SDK is built on top of GraphQL, it inherits the concept of
edgesand
nodes.
Learn more about pagination in GraphQL.
The following example shows how you can paginate through products in a collection:
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()
The corresponding GraphQL query looks like this:
query { node(id: "IjoxNDg4MTc3MzEsImxhc3R") { ... on Collection { products(first: 10, after: "sdWUiOiIxNDg4MTc3M") { pageInfo { hasNextPage } edges { cursor node { id title productType description } } } } } }
Since we know exactly what collection we want to fetch products for, we'll use the
nodeinterface to query the collection by
id. You might have also noticed that we're fetching a couple of additional fields and objects:
pageInfoand
cursor. We can then use a
cursorof any product edge to fetch more products
beforeit or
afterit. Likewise, the
pageInfoobject provides additional metadata about whether the next page (and potentially previous page) is available or not.
In our sample app we likely want to have a detailed product page with images, variants, and descriptions. Conventionally, we'd need multiple REST calls to fetch all the required information. But with the Buy SDK, we can do it with a single query:
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()
The corresponding GraphQL query looks like this:
{ node(id: "9Qcm9kdWN0LzMzMj") { ... on Product { id title description images(first: 10) { edges { node { id src } } } variants(first: 10) { edges { node { id price title available } } } } } }
After browsing products and collections, a customer might eventually want to purchase something. The Buy SDK does not provide support for a local shopping cart since the requirements can vary between applications. Instead, the implementation is left up to the custom storefront. Nevertheless, when a customer is ready to make a purchase you'll need to create a checkout.
Almost every
mutationrequest requires an input object. This is the object that dictates what fields will be mutated for a particular resource. In this case, we'll need to create a
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), ]) )
The checkout input object accepts other arguments like
shippingAddress. In our example we don't have access to that information from the customer until a later time, so we won't include them in this mutation. Given the checkout input, we can execute the
checkoutCreatemutation:
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()
It is best practice to always include
userErrorsfields in your mutation payload query, where possible. You should always validate user input before making mutation requests, but it's possible that a validated user input might cause a mismatch between the client and server. In this case,
userErrorscontains an error with a
fieldand
messagefor any invalid or missing fields.
Since we'll need to update the checkout with additional information later, all we need from a checkout in this mutation is an
idso we can keep a reference to it. We can skip all other fields on
Storefront.Checkoutfor efficiency and reduced bandwidth.
A customer's information might not be available when a checkout is created. The Buy SDK provides mutations for updating the specific checkout fields that are required for completion: the
shippingAddressand
shippingLinefields.
Note that if your checkout contains a line item that requires shipping, you must provide a shipping address and a shipping line as part of your checkout.
To obtain the handle required for updating a shipping line, you must first poll for shipping rates.
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() } } }
Available shipping rates are specific to a checkout since the cost to ship items depends on the quantity, weight, and other attributes of the items in the checkout. Shipping rates also require a checkout to have a valid
shippingAddress, which can be updated using steps found in updating a checkout. Available shipping rates are a field on
Storefront.Checkout, so given a
checkoutID(that we kept a reference to earlier) we can query for shipping rates:
let query = Storefront.buildQuery { $0 .node(id: checkoutID) { $0 .onCheckout { $0 .id() .availableShippingRates { $0 .ready() .shippingRates { $0 .handle() .price() .title() } } } } }
The query above starts an asynchronous task on the server to fetch shipping rates from multiple shipping providers. Although the request might return immediately (network latency aside), it does not mean that the list of shipping rates is complete. This is indicated by the
readyfield in the query above. It is your application's responsibility to continue retrying this query until
ready == true. The Buy SDK has built-in support for retrying requests, so we'll create a retry handler and perform the query:
let retry = Graph.RetryHandler(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() </storefront.queryroot>
The completion will be called only if
availableShippingRates.ready == trueor the retry count reaches 10. Although you can specify
.infinitefor the retry handler's
enduranceproperty, we highly recommend you set a finite limit.
let mutation = Storefront.buildMutation { $0 .checkoutShippingLineUpdate(checkoutId: id, shippingRateHandle: shippingRate.handle) { $0 .checkout { $0 .id() } .userErrors { $0 .field() .message() } } }
After all required fields have been filled and the customer is ready to pay, you have three ways to complete the checkout and process the payment:
The simplest way to complete a checkout is by redirecting the customer to a web view where they will be presented with the same flow that they're familiar with on the web. The
Storefront.Checkoutresource provides a
webUrlthat you can use to present a web view. We highly recommend using
SFSafariViewControllerinstead of
WKWebViewor other alternatives.
NOTE: Although using web checkout is the simplest out of the 3 approaches, it presents some difficulty when it comes to observing the checkout state. Since the web view doesn't provide any callbacks for various checkout states, you still need to poll for checkout completion.
The native credit card checkout offers the most conventional user experience out of the three alternatives, but it also requires the most effort to implement. You'll be required to implement UI for gathering your customers' name, email, address, payment information and other fields required to complete checkout.
Assuming your custom storefront has all the information it needs, the first step to completing a credit card checkout is to vault the provided credit card and exchange it for a payment token that will be used to complete the payment. To learn more, see the instructions for vaulting a credit card.
After obtaining a credit card vault token, we can proceed to complete the checkout by creating a
CreditCardPaymentInputand executing the mutation query:
// let paySession: PaySession // let payAuthorization: PayAuthorization // let moneyInput: MoneyInputlet 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()
IMPORTANT: Before completing the checkout with a credit card, you need to have the
write_checkouts_paymentsscope enabled for your app. This can be done by requesting payment process for native mobile apps. Alternatively, if this will be a Sales Channel, you can request through payment processing for Sales Channels.
3D Secure Checkout
To implement 3D secure on your checkout flow, see the API Help Docs.
IMPORTANT: Before completing the checkout with an Apple Pay token you should ensure that your checkout is updated with the latests shipping address provided by
paySession(_:didAuthorizePayment:checkout:completeTransaction:)delegate callback. If you've previously set a partial shipping address on the checkout for obtaining shipping rates, the current checkout, as is, will not complete successfully. You must update the checkout with the full address that includes address lines and a complete zip or postal code.
The Buy SDK makes Pay integration easy with the provided
Pay.framework. To learn how to set up and use
PaySessionto obtain a payment token, refer to the Pay section. After we have a payment token, we can complete the checkout:
// let paySession: PaySession // let payCheckout: PayCheckout // let payAuthorization: PayAuthorizationlet payment = Storefront.TokenizedPaymentInput.create( amount: payCheckout.paymentDue, idempotencyKey: paySession.identifier, billingAddress: self.mailingAddressInputFrom(payAuthorization.billingAddress), //
Polling for checkout completion ⤴
After a successful
checkoutCompleteWith...mutation, the checkout process starts. This process is usually short, but it isn't immediate. Because of this, polling is required to obtain an updated checkout in areadystate - with aStorefront.Order.let retry = Graph.RetryHandler(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() </storefront.queryroot>
Again, just like when polling for available shipping rates, we need to create a
RetryHandlerto provide a condition upon which to retry the request. In this case, we're asserting that theStorefront.Orderisnil, and we'll continue to retry the request if it is.Handling errors ⤴
The
Graph.Clientcan return a non-nilGraph.QueryError. The error and the result are not mutually exclusive. It is valid to have both an error and a result. However, the errorcase, in this instance, is always.invalidQuery(let reasons). You should always evaluate theerror, make sure that you don't have an invalid query, and then evaluate the result:let task = self.client.queryGraphWith(query) { result, error inif 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()
IMPORTANT:
Graph.QueryErrordoes not contain user-friendly information. Often, it describes the technical reason for the failure, and shouldn't be shown to the end-user. Handling errors is most useful for debugging.Customer Accounts ⤴
Using the Buy SDK, you can build custom storefronts that let your customers create accounts, browse previously completed orders, and manage their information. Since most customer-related actions modify states on the server, they are performed using various
mutationrequests. Let's take a look at a few examples.Creating a customer ⤴
Before a customer can log in, they must first create an account. In your application, you can provide a sign-up form that runs the following
mutationrequest. In this example, theinputfor the mutation is some basic customer information that will create an account on your shop.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() } } }
Keep in mind that this mutation returns a
Storefront.Customerobject, not an access token. After a successful mutation, the customer will still be required to log in using their credentials.Customer login ⤴
Any customer who has an account can log in to your shop. All log-in operations are
mutationrequests that exchange customer credentials for an access token. You can log in your customers using thecustomerAccessTokenCreatemutation. Keep in mind that the return access token will eventually expire. The expiryDateis provided by theexpiresAtproperty of the returned payload.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() } } }
Optionally, you can refresh the custom access token periodically using the
customerAccessTokenRenewmutation.IMPORTANT: It is your responsibility to securely store the customer access token. We recommend using Keychain and best practices for storing secure data.
Password reset ⤴
Occasionally, a customer might forget their account password. The SDK provides a way for your application to reset a customer's password. A minimalistic implementation can simply call the recover mutation, at which point the customer will receive an email with instructions on how to reset their password in a web browser.
The following mutation takes a customer's email as an argument and returns
userErrorsin the payload if there are issues with the input:let mutation = Storefront.buildMutation { $0 .customerRecover(email: "[email protected]") { $0 .userErrors { $0 .field() .message() } } }Create, update, and delete address ⤴
You can create, update, and delete addresses on the customer's behalf using the appropriate
mutation. Keep in mind that these mutations require customer authentication. Each query requires a customer access token as a parameter to perform the mutation.The following example shows a mutation for creating an address:
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() } } }
Customer information ⤴
Up to this point, our interaction with customer information has been through
mutationrequests. At some point, we'll also need to show the customer their information. We can do this using customerqueryoperations.Just like the address mutations, customer
queryoperations are authenticated and require a valid access token to execute. The following example shows how to obtain some basic customer info:let query = Storefront.buildQuery { $0 .customer(customerAccessToken: token) { $0 .id() .firstName() .lastName() .email() } }Customer Addresses ⤴
You can obtain the addresses associated with the customer's account:
let query = Storefront.buildQuery { $0 .customer(customerAccessToken: token) { $0 .addresses(first: 10) { $0 .edges { $0 .node { $0 .address1() .address2() .city() .province() .country() } } } } }Customer Orders ⤴
You can also obtain a customer's order history:
let query = Storefront.buildQuery { $0 .customer(customerAccessToken: token) { $0 .orders(first: 10) { $0 .edges { $0 .node { $0 .id() .orderNumber() .totalPrice() } } } } }Customer Update ⤴
Input objects, like
Storefront.MailingAddressInput, useInput(whereTis the type of value) to represent optional fields and distinguishnilvalues fromundefinedvalues (eg.phone: Input).The following example uses
Storefront.CustomerUpdateInputto show how to update a customer's phone number:let input = Storefront.CustomerUpdateInput( phone: .value("+16471234567") )In this example, you create an input object by setting the
phonefield to the new phone number that you want to update the field with. Notice that you need to pass in anInput.value()instead of a simple string containing the phone number.The
Storefront.CustomerUpdateInputobject also includes other fields besides thephonefield. These fields all default to a value of.undefinedif you don't specify them otherwise. This means that the fields aren't serialized in the mutation, and will be omitted entirely. The result looks like this:mutation { customerUpdate(customer: { phone: "+16471234567" }, customerAccessToken: "...") { customer { phone } } }This approach works well for setting a new phone number or updating an existing phone number to a new value. But what if the customer wants to remove the phone number completely? Leaving the phone number blank or sending an empty string are semantically different and won't achieve the intended result. The former approach indicates that we didn't define a value, and the latter returns an invalid phone number error. This is where the
Inputis especially useful. You can use it to signal the intention to remove a phone number by specifying anilvalue:let input = Storefront.CustomerUpdateInput( phone: .value(nil) )The result is a mutation that updates a customer's phone number to
null.mutation { customerUpdate(customer: { phone: null }, customerAccessToken: "...") { customer { phone } } }Sample application ⤴
For help getting started, take a look at the sample iOS app. It covers the most common use cases of the SDK and how to integrate with it. Use the sample app as a template, a starting point, or a place to cherrypick components as needed. Refer to the app's readme for more details.
Contributions ⤴
We welcome contributions. Please follow the steps in our contributing guidelines.
Help ⤴
For help with the Mobile Buy SDK, see the iOS Buy SDK documentation or post questions on our forum, in the
Shopify APIs & SDKssection.License ⤴
The Mobile Buy SDK is provided under an MIT License.