Shopify’s Mobile Buy SDK makes it simple to sell physical products inside your mobile app.

Overview

Mobile Buy SDK

Build GitHub release Carthage compatible Swift Package Manager compatible GitHub license

Mobile Buy SDK

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.

Table of contents

Documentation

You can generate a complete HTML and .docset documentation by running the Documentation scheme. You can then use a documentation browser like Dash to access the .docset artifact, or browse the HTML directly in the Docs/Buy and Docs/Pay directories.

The documentation is generated using Jazzy.

Installation

Download the latest version

Swift Package Manager

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.

Dynamic Framework Installation

  1. Add Buy as a git submodule by running:
git submodule add [email protected]:Shopify/mobile-buy-sdk-ios.git
  1. Ensure that all submodules of Buy SDK have also been updated by running:
git submodule update --init --recursive
  1. Drag the Buy.xcodeproj into your application project.
  2. Add Buy.framework target as a dependency:
    1. Navigate to Build Phases > Target Dependencies.
    2. Add Buy.framework.
  3. Link Buy.framework:
    1. Navigate to Build Phases > Link Binary With Libraries.
    2. Add Buy.framework.
  4. Make sure that the framework is copied into the bundle:
    1. Navigate to Build Phases > New Copy Files Phase.
    2. From the Destination dropdown, select Frameworks.
    3. Add Buy.framework.
  5. Import into your project files using import Buy.

See the Storefront sample app for an example of how to add the Buy target a dependency.

Carthage

  1. Add the following line to your Cartfile:
github "Shopify/mobile-buy-sdk-ios"
  1. Run carthage update.
  2. Follow the steps to link the dynamic framework that Carthage produced.
  3. Import the SDK module:
import Buy

CocoaPods

  1. Add the following line to your podfile:
pod "Mobile-Buy-SDK"
  1. Run pod install.
  2. Import the SDK module:
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

Getting started

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.

Migration from SDK v2.0

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.

Code Generation

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.

Request Models

All generated request models are derived from the GraphQL.AbstractQuery type. 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 this

let 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
  }
}

Response models

All generated response models are derived from the GraphQL.AbstractResponse type. This abstract type provides a similar key-value type interface to a Dictionary for 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.QueryRoot

let name: String = response.shop.name

Never use the abstract class directly:

// Never do this

let 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.

The Node protocol

The GraphQL schema defines a Node interface that declares an id field 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 Node type 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.Order requires a cast:

// response: Storefront.QueryRoot

let order = response.node as! Storefront.Order

Aliases

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.QueryRoot

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

Learn more about GraphQL aliases.

Graph.Client

The Graph.Client is a network layer built on top of URLSession that executes query and mutation requests. It also simplifies polling and retrying requests. To get started with Graph.Client, you need the following:

  • Your shop's .myshopify.com domain
  • Your API key, which you can find in your shop's admin page
  • A URLSession (optional), if you want to customize the configuration used for network requests or share your existing URLSession with the Graph.Client
  • (optional) The buyer's current locale. Supported values are limited to locales available to your shop.
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 Client exposes these as two type-safe operations, although it also offers some conveniences for retrying and polling in each one.

Queries

Semantically, a GraphQL query operation is equivalent to a GET RESTful 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.

Mutations

Semantically a GraphQL mutation operation is equivalent to a PUT, POST or DELETE RESTful 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 mutation as 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 query request.

With Graph.Client you 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 userErrors field 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 address to Password is too short.

Learn more about GraphQL mutations.

Retry

Both queryGraphWith and mutateGraphWith accept an optional RetryHandler . This object encapsulates the retry state and customization parameters for how the Client will retry subsequent requests (such as after a set delay, or a number of retries). By default, the retryHandler is nil and no retry behavior will be provided. To enable retry or polling, create a handler with a condition. If the handler.condition and handler.canRetry evaluate to true, then the Client will continue executing the request:

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
}

The retry handler is generic, and can handle both query and mutation requests equally well.

Caching

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 POST requests, we can't easily take advantage of the HTTP caching that's available in URLSession. For this reason, the Graph.Client is 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 query operations. It isn't available for mutation operations 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.

Enable client-wide caching

You can enable client-wide caching by providing a default cachePolicy for any instance of Graph.Client. This sets all query operations to use your default cache policy, unless you specify an alternate policy for an individual request.

In this example, we set the client's cachePolicy property to cacheFirst:

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

Now, all calls to queryGraphWith will yield a task with a .cacheFirst cache 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 task cache 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.

Errors

The completion for either a query or mutation request will always contain an optional Graph.QueryError that represents the current error state of the request. It's important to note that error and response are 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.QueryError is 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 Reason objects 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.

Search

Some Storefront models accept search terms via the query parameter. For example, you can provide a query to 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()
                }
            }
        }
    }
}

Fuzzy matching

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.

Field matching

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_term will be an exact match instead of a fuzzy match. For example, based on the query above, a collection with the type blue_runners will not match the query for runners.

Negating field matching

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
    ...
}

Boolean operators

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 blue and 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
    ...
}

Comparison operators

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:

\"2017-05-29T00:00:00Z\"") { $0 ... } ">
.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.

Exists operator

There is one special operator that can be used for checking nil or 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
    ...
}

Card Vaulting

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.

Card Client

Like Graph.Client, the Card.Client manages 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.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()

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 token is used to complete a checkout.

Apple Pay

Support for  Pay is provided by the Pay framework. It is compiled and tested separately from the Buy SDK 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.

Pay Session

When the customer is ready to pay for products in your application with  Pay, the PaySession encapsulates all the states necessary to complete the checkout process:

  • your shop's currency
  • your  Pay merchantID
  • available shipping rates
  • selected shipping rate
  • billing & shipping address
  • checkout state

To present the  Pay modal and begin the checkout process, you need:

  • a created Storefront.Checkout
  • currency information that can be obtained with a query on Storefront.Shop
  •  Pay merchantID

After all the prerequisites have been met, you can initialize a PaySession and 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 PKPaymentAuthorizationController on 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.Checkout with appropriate mutations. This keeps the checkout state on the server up-to-date.

Let's take a look at each one:

Did update shipping address

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 PayPostalAddress is a partial address that excludes the street address for added security. This is actually enforced by PassKit and not the Pay framework. Nevertheless, information contained in PayPostalAddress is sufficient to obtain an array of available shipping rates from Storefront.Checkout.

Did select shipping rate

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 delegate callback.

Did authorize payment

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 delegate will receive the encrypted token and other associated information that you'll need for the final completeCheckout mutation 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.

Did finish payment

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.

Case studies

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

Fetch shop

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
    }
  }
}

Fetch collections and products

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 + 1 problem.

The Buy SDK is built on GraphQL, which solves the n + 1 request 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?

Pagination

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 edges and 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 node interface to query the collection by id. You might have also noticed that we're fetching a couple of additional fields and objects: pageInfo and cursor. We can then use a cursor of any product edge to fetch more products before it or after it. Likewise, the pageInfo object provides additional metadata about whether the next page (and potentially previous page) is available or not.

Fetch product details

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
          }
        }
      }
    }
  }
}

Checkout

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 mutation request 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 email and 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 checkoutCreate mutation:

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 userErrors fields 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, userErrors contains an error with a field and message for 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 id so we can keep a reference to it. We can skip all other fields on Storefront.Checkout for efficiency and reduced bandwidth.

Updating a checkout

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 email, shippingAddress and shippingLine fields.

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.

Updating email
let mutation = Storefront.buildMutation { $0
    .checkoutEmailUpdate(checkoutId: id, email: "[email protected]") { $0
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}
Updating shipping address
let shippingAddress: Storefront.MailingAddressInput
let mutation = Storefront.buildMutation { $0
    .checkoutShippingAddressUpdate(shippingAddress: shippingAddress, checkoutId: id) {
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}
Polling for checkout readiness

Checkouts may have asynchronous operations that can take time to finish. If you want to complete a checkout or ensure all the fields are populated and up to date, polling is required until the ready value is true. Fields that are populated asynchronously include duties and taxes.

All asynchronous computations are completed and the checkout is updated accordingly once the checkout.ready flag is true. This flag should be checked (and polled if it is false) after every update to the checkout to ensure there are no asynchronous processes running that could affect the fields of the checkout. Common examples would be after updating the shipping address or adjusting the line items of a checkout.

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()
            }
            // ...
        }
    }
}

It is your application's responsibility to poll for updates and to continue retrying this query until ready == true and to use the updated fields returned with that response. 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<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()

The completion will be called only if checkout.ready == true or the retry count reaches 10. Although you can specify .infinite for the retry handler's endurance property, we highly recommend you set a finite limit.

Polling for shipping rates

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 ready field 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<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()

The completion will be called only if availableShippingRates.ready == true or the retry count reaches 10. Although you can specify .infinite for the retry handler's endurance property, we highly recommend you set a finite limit.

Updating shipping line
let mutation = Storefront.buildMutation { $0
    .checkoutShippingLineUpdate(checkoutId: id, shippingRateHandle: shippingRate.handle) { $0
        .checkout { $0
            .id()
        }
        .userErrors { $0
            .field()
            .message()
        }
    }
}

Completing a checkout

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:

Web checkout

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.Checkout resource provides a webUrl that you can use to present a web view. We highly recommend using SFSafariViewController instead of WKWebView or 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.

Credit card checkout

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 CreditCardPaymentInput and executing the mutation query:

// 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 Checkout

To implement 3D secure on your checkout flow, see the API Help Docs.

Apple Pay checkout

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 PaySession to 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: 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()

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 a ready state - with a 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()

Again, just like when polling for available shipping rates, we need to create a RetryHandler to provide a condition upon which to retry the request. In this case, we're asserting that the Storefront.Order is nil, and we'll continue to retry the request if it is.

Handling errors

The Graph.Client can return a non-nil Graph.QueryError. The error and the result are not mutually exclusive. It is valid to have both an error and a result. However, the error case, in this instance, is always .invalidQuery(let reasons). You should always evaluate the error, make sure that you don't have an invalid query, and then evaluate the result:

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()

IMPORTANT: Graph.QueryError does 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 mutation requests. 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 mutation request. In this example, the input for 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.Customer object, 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 mutation requests that exchange customer credentials for an access token. You can log in your customers using the customerAccessTokenCreate mutation. Keep in mind that the return access token will eventually expire. The expiry Date is provided by the expiresAt property 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 customerAccessTokenRenew mutation.

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 userErrors in 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 mutation requests. At some point, we'll also need to show the customer their information. We can do this using customer query operations.

Just like the address mutations, customer query operations 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, use Input (where T is the type of value) to represent optional fields and distinguish nil values from undefined values (eg. phone: Input ).

The following example uses Storefront.CustomerUpdateInput to 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 phone field to the new phone number that you want to update the field with. Notice that you need to pass in an Input.value() instead of a simple string containing the phone number.

The Storefront.CustomerUpdateInput object also includes other fields besides the phone field. These fields all default to a value of .undefined if 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 Input is especially useful. You can use it to signal the intention to remove a phone number by specifying a nil value:

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 & SDKs section.

License

The Mobile Buy SDK is provided under an MIT License.

Comments
  • Mobile-buy-sdk forbidden issue

    Mobile-buy-sdk forbidden issue

    Hello experts,

    I am new to shopify. Downloaded mobile-buy-sdk and integrate in my project by following the steps given in doc. I use sample app provided with this sdk to test if my store is working or not.

    Now in sample app I just replace my apiKey in Client.swift but I am always getting the following error:

    Exercising cache policy: CACHE_FIRST(3600)
    Exercising cache policy: NETWORK_ONLY
    Graph.QueryError: http(403)
    Failed to load collections: Optional(Buy.Graph.QueryError.http(403))
    

    Can anyone else tell me what else I need to set up to get details of my store's data in this sample app.

    Also do I need to set Storefront Access token in my app?

    Can any one help me to resolve this issue.

    Thanks in advance

    3.x question 
    opened by thousandzerokanika 28
  • Calculated Shipping Rates in BUY SDK inconsistently causing crash

    Calculated Shipping Rates in BUY SDK inconsistently causing crash

    Whenever I use the call getShippingRatesForCheckout(withToken:) the app crashes to app delegate. No output or error returns. This only occurs when my shipping zone is set to calculated rates (USPS). If I use weight based, or price based everything works fine.

    I found an issue from 2015 that complained about the exact same thing but was later closed as fixed. It doesn't even appear to be a per store issue. Other stores we've created are crashing for some products and not others. Not sure if its a Shopify API issue or an issue with USPS. If anyone has any insight on this I would be very grateful.

    opened by MrGobert 22
  • Checkout failed: Shipping rate can't be blank

    Checkout failed: Shipping rate can't be blank

    None of the above? Create an issue. Be sure to include all the necessary information for us to understand and reproduce the problem:

    I've seen a few older posts about this but no solutions were helpful. I've followed the readme and also copied the functions and code almost verbatim (minus the view models) from the example app in this repo, yet when I try to checkout with Apple Pay, I still get the error

    Optional([<CheckoutUserError: ["message": Shipping rate can't be blank, "field": ]>, <CheckoutUserError: ["message": Shipping line can't be blank, "field": ]>]) Checkout failed to complete.

    SDK Version 3.6.0

    In my CartViewController:

    extension CartViewController: PaySessionDelegate {
    
        private func convert(checkout: Storefront.Checkout) -> PayCheckout {
            let lineItems: [PayLineItem] = checkout.lineItems.edges.map { item  in
                let price = item.node.variant!.priceV2.amount
                let quantity = item.node.quantity
                return PayLineItem(price: price, quantity: Int(quantity))
            }
    
            let shippingAddress = PayAddress(
                addressLine1: checkout.shippingAddress?.address1,
                addressLine2: checkout.shippingAddress?.address2,
                city: checkout.shippingAddress?.city,
                country: checkout.shippingAddress?.country,
                province: checkout.shippingAddress?.province,
                zip: checkout.shippingAddress?.zip,
                firstName: checkout.shippingAddress?.firstName,
                lastName: checkout.shippingAddress?.lastName,
                phone: checkout.shippingAddress?.phone,
                email: nil
            )
    
            let (subtotal, tax, shipping, total) = PaymentManager.paymentParts(subtotal: self.subtotal)
    
            let shippingRate: PayShippingRate
            if let shippingLine = checkout.shippingLine {
    // this hits automatically as Apple Pay seems to select the cheapest shipping rate. It also hits when the user explicitly selects a shipping rate
                shippingRate = PayShippingRate(handle: shippingLine.handle, title: shippingLine.handle, price: shippingLine.priceV2.amount)
            } else {
                shippingRate = PayShippingRate(handle: "Default Shipping", title: "Default Shipping", price: shipping)
            }
    
            return PayCheckout(
                id: checkout.id.rawValue,
                lineItems: lineItems,
                giftCards: nil,
                discount: nil,
                shippingDiscount: nil,
                shippingAddress: shippingAddress,
                shippingRate: shippingRate,
                currencyCode: "USD",
                subtotalPrice: subtotal,
                needsShipping: true,
                totalTax: tax,
                paymentDue: total
            )
        }
    
        private func convertShippingRates(_ rates: [Storefront.ShippingRate]) -> [PayShippingRate] {
            return rates.map { PayShippingRate(handle: $0.handle, title: $0.title, price: $0.priceV2.amount) }
        }
    
        func paySession(_ paySession: PaySession, didRequestShippingRatesFor address: PayPostalAddress, checkout: PayCheckout, provide: @escaping (PayCheckout?, [PayShippingRate]) -> Void) {
            print("didRequestShippingRatesFor")
            print("Updating checkout with address...")
            ShopifyClient.shared.updateCheckout(checkout.id, updatingPartialShippingAddress: address) { checkout in
                guard let checkout = checkout else {
                    print("Update for checkout failed.")
                    provide(nil, [])
                    return
                }
    
                print("Getting shipping rates...")
                ShopifyClient.shared.fetchShippingRatesForCheckout(checkout.id.rawValue) { result in
                    if let result = result {
                        print("Fetched shipping rates.")
                        let payCheckout = self.convert(checkout: result.checkout)
                        let rates = self.convertShippingRates(result.rates)
                        provide(payCheckout, rates)
                    } else {
                        provide(nil, [])
                    }
                }
            }
        }
    
        func paySession(_ paySession: PaySession, didUpdateShippingAddress address: PayPostalAddress, checkout: PayCheckout, provide: @escaping (PayCheckout?) -> Void) {
            print("Updating checkout with shipping address for tax estimate...")
            ShopifyClient.shared.updateCheckout(checkout.id, updatingPartialShippingAddress: address) { checkout in
                if let checkout = checkout {
                    let payCheckout = self.convert(checkout: checkout)
                    provide(payCheckout)
                } else {
                    print("Update for checkout failed.")
                    provide(nil)
                }
            }
        }
    
        func paySession(_ paySession: PaySession, didSelectShippingRate shippingRate: PayShippingRate, checkout: PayCheckout, provide: @escaping (PayCheckout?) -> Void) {
            print("Selecting shipping rate...")
            ShopifyClient.shared.updateCheckout(checkout.id, updatingShippingRate: shippingRate) { updatedCheckout in
                print("Selected shipping rate.")
                let payCheckout = self.convert(checkout: updatedCheckout!)
                provide(payCheckout)
            }
        }
    
        func paySession(_ paySession: PaySession, didAuthorizePayment authorization: PayAuthorization, checkout: PayCheckout, completeTransaction: @escaping (PaySession.TransactionStatus) -> Void) {
            // I have an `authorization.shippingRate` here... and a good `authorization.shippingAddress`
    
            guard let email = authorization.shippingAddress.email else {
                print("Unable to update checkout email. Aborting transaction.")
                return completeTransaction(.failure)
            }
    
            ShopifyClient.shared.updateCheckout(checkout.id, updatingCompleteShippingAddress: authorization.shippingAddress) { updatedCheckout in
                guard let _ = updatedCheckout else {
                    print("unable to update shipping address")
                    return completeTransaction(.failure)
                }
    
                print("Updating checkout email...")
                ShopifyClient.shared.updateCheckout(checkout.id, updatingEmail: email) { updatedCheckout in
                    guard let _ = updatedCheckout else {
                        print("unable to update checkout email, aborting txn.")
                        return completeTransaction(.failure)
                    }
                    
                    print("Checkout email updated: \(email)")
                    print("Completing checkout...")
    
                    ShopifyClient.shared.completeCheckout(checkout, billingAddress: authorization.billingAddress, applePayToken: authorization.token, idempotencyToken: paySession.identifier) { payment in
                        if let payment = payment, checkout.paymentDue == payment.amountV2.amount {
                            print("Checkout completed successfully.")
                            completeTransaction(.success)
                        } else {
                            print("Checkout failed to complete.")
                            completeTransaction(.failure)
                        }
                    }
                }
            }
        }
    
        func paySessionDidFinish(_ paySession: PaySession) {
            // Do something after the  Pay modal is dismissed
            print("didFinish")
        }
    
    }
    

    In my ShopifyClient

    func completeCheckout(_ checkout: PayCheckout, billingAddress: PayAddress, applePayToken token: String, idempotencyToken: String, completion: @escaping (Storefront.Payment?) -> Void) {
            let mutation = ClientQuery.mutationForCompleteCheckoutUsingApplePay(checkout, billingAddress: billingAddress, token: token, idempotencyToken: idempotencyToken)
    
            let task = self.client.mutateGraphWith(mutation) { response, error in
                error.debugPrint()
    
                print(response?.checkoutCompleteWithTokenizedPaymentV2?.checkoutUserErrors) // errors appear here
    
                if let payment = response?.checkoutCompleteWithTokenizedPaymentV2?.payment {
                    print("Payment created, fetching status...")
                    self.fetchCompletedPayment(payment.id.rawValue) { paymentViewModel in
                        completion(paymentViewModel)
                    }
    
                } else {
                    completion(nil)
                }
            }
    
            task.resume()
    }
    

    Expected: Apple Pay checkout works

    Actual: I keep getting the error above, even when I go into the Apple Pay prompt and explicitly select the shipping option.

    As I mentioned before, I just copied identically what happens in the example app except for the view models. I return the actual Storefront. objects instead.

    I'd love to know what I'm doing wrong here. Let me know if you need to see any additional code

    3.x information required pay 
    opened by zackshapiro 17
  • Checkout Complete using CreditCard option  access denied

    Checkout Complete using CreditCard option access denied

    i am using credit card option for payment. i got below error in test credit card with test mode

    MobileBuySDK.Graph.QueryError.invalidQuery([MobileBuySDK.Graph.QueryError.Reason(message: "CheckoutCompleteWithCreditCard access denied", line: nil, column: nil)])

    taken a code from reference link https://github.com/Shopify/mobile-buy-sdk-ios#credit-card-checkout-

    3.x api 
    opened by networld301ios 17
  • "Payfort - Start" Payment gateway not working with Credit card option

    Hi , We are developing iOS and Android app for shopfy store. We have integrated MobileBuy SDK. When we tried to do native checkout with credit card we are getting below error

    Error Domain=BUYShopifyErrorDomain Code=422 "(null)" UserInfo={errors={
       "payment_gateway" =     (
                   {
               code = invalid;
               message = "is invalid";
               options =             {
               };
           }
       );
    }}
    

    We have integrated “Payfort - Start” payment gateway which listed in Payment Gateways Supported document.

    2.x api 
    opened by AkashDoshi19 17
  • enable to find default  Address from multiple Addresses.

    enable to find default Address from multiple Addresses.

    hi i am try to getting default address from multiple addresses. using below query

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

    can you give a field for as a default address.

    3.x question 
    opened by networld301ios 16
  • Note field on Checkout Object is not updated

    Note field on Checkout Object is not updated

    As a title says, I'm getting Checkout object with initial value of .note(). Note can be updated through the web page, but still I'll receive an object with initial value of .note(). I've tried to download Checkout object in Postman and it returns updated value.

    3.x 
    opened by stolkachov 15
  • Crash on all API calls

    Crash on all API calls

    Hey, so I added the mobile SDK (as static framework) to an app today, following the documentation guidelines. It looks like the BUYClient initializes but anything I do like get collection-1 or get a product by ID just crashes on this: screen shot 2016-11-09 at 6 14 20 pm screen shot 2016-11-09 at 6 14 53 pm And this is literally the only code I have: shopifyClient = [[BUYClient alloc] initWithShopDomain:@"XXXXXXXXXXX.myshopify.com" apiKey:@"XXXXXXXXXXXX" appId:@"X"]; [shopifyClient getProductById:@(XXXXXXXXX) completion:^(BUYProduct * _Nullable product, NSError * _Nullable error) { NSLog(@"hi"); // never reached }]; (Tested on iOS 10 simulator and iOS 10 phone). --> Similar crash happens with even the basic "getCollectionsPage:1" method.

    Not sure what I am doing wrong... the online implementation we have works with our collection/products... Any help is appreciated.

    opened by cclogg 15
  • Ability to View Resource Metafields

    Ability to View Resource Metafields

    A powerful feature of Shopify is the ability to add metafields to products, collections, etc. These fields are available via liquid to be used within our web shops, but they are currently not available via the mobile SDK.

    Are there any plans to add them?

    enhancement 2.x 3.x 
    opened by cocoahero 15
  • isApplePayAvailable loses state on memory warning

    isApplePayAvailable loses state on memory warning

    I'm integrating Shopify for a sizable client of ours. I spoke with a shopify engineer around a month ago by phone on this issue but he was still new on the iOS team and wasn't able to answer this issue...

    What happens is, the bool, isApplePayAvailable, loses state when a memory issue occurs. Why is this bad? Once isApplePayAvailable is set to 0 and the button reverts to "Set up apple pay", the user is taken to the credit card setup every time. Somehow, [PKPaymentAuthorizationViewController canMakePayments] becomes unset.

    NOTE: BOOL didIntegrationSucceed = [client testIntegrationWithMerchantId:MERCHANT_ID]; will always return true even with the memory issue. No issues there.

    Does anyone have any insight? Im about to ditch Shopify for the client if I cant figure this out. The issue has been around for the past 3 releases now (3 months?).

    opened by bmgdev 14
  • Module compiled with Swift 5.2.4 cannot be imported by the Swift 5.3 compiler on Xcode 12

    Module compiled with Swift 5.2.4 cannot be imported by the Swift 5.3 compiler on Xcode 12

    Description

    Previously I was using Xcode 11.6 to build these frameworks using Carthage. Now I upgraded Xcode to 12 and after building these frameworks when I integrate into my project and tried to run, it shows an error like:

    Module compiled with Swift 5.2.4 cannot be imported by the Swift 5.3 compiler

    Steps to Reproduce

    1. update your Xcode version to 12.0
    2. Build framework using Carthage carthage update --platform iOS (version 5.0.0)
    3. Add Buy.framework and Pay.framework into any project
    4. Run the project

    Actual Error:

    /Users/John/Desktop/Shopify Demo/API/ClientQuery.swift:4:8: Module compiled with Swift 5.2.4 cannot be imported by the Swift 5.3 compiler: /Users/John/Desktop/Shopify Demo/Shopify Demo/Buy.framework/Modules/Buy.swiftmodule/x86_64-apple-ios-simulator.swiftmodule

    Workarounds I tried:

    1. So few workarounds like this where you need to set the Build Libraries for Distribution option to Yes in your framework's build settings, otherwise the swift compiler doesn't generate the neccessary .swiftinterface files which are the key to future compilers being able to load your old library. But it didn't solve the issue.
    2. Next I tried this Carthage workaround where you need to tweak the EXCLUDED_ARCHS statement. But it also failed.

    Versions

    1. Xcode v 12.0 (12A7209)
    2. Carthage v 0.36.0
    3. Shopify mobile-buy-sdk-ios v 5.0.0
    opened by das-palash-appsbee 13
  • Issue in Webcheckout

    Issue in Webcheckout

    Hi, I am using web checkout in my application.

    • First i m calling the create-checkout api to create a checkout
    • After that i m using checkoutCustomerAssociateV2 to associate the logged in user with the checkout.

    The issue if i logout from the application and login again with some other user it is still maintaining session of my previous logged in user.

    https://user-images.githubusercontent.com/47588773/197335178-05a98810-fbb4-4c7f-b8b5-e33d50dd0607.mp4

    opened by ArohiMagotra 2
  • Support international pricing on storefronts

    Support international pricing on storefronts

    Hello team!

    I was looking for Internationalization feature and couldn’t figure it out.

    • Version of the SDK are you using "Mobile-Buy-SDK", '~> 7.1.0'

    • What is the expected behaviour? I'm referring to this(https://shopify.dev/api/examples/international-pricing) documentation, and as it says 'To retrieve international prices for products, specify the @inContext(country: countryCode) directive in your query.'
 I'm doing the same, tried sending countryCode as IN i.e India, i should receive values in Indian rupees

    • What is happening instead? insted i'm receing values in USD

    • Screenshot (if applicable) https://mobile-buy-sdk-ios-issue.s3.ap-south-1.amazonaws.com/+response.png https://mobile-buy-sdk-ios-issue.s3.ap-south-1.amazonaws.com/request.png

    https://mobile-buy-sdk-ios-issue.s3.ap-south-1.amazonaws.com/%5B8375%5D+Response+-+plobal-test-mobile-filter.myshopify.com_api_2022-04_graphql.txt https://mobile-buy-sdk-ios-issue.s3.ap-south-1.amazonaws.com/%5B8375%5D+Request+-+plobal-test-mobile-filter.myshopify.com_api_2022-04_graphql.txt

    I look forward to hearing from you as soon as possible to resolve this problem.

    opened by BudhabhooshanPatil 0
  • Get checkout (id) from checkout link

    Get checkout (id) from checkout link

    I'm trying to implement the selling plans on my app, I've moved all the logic that were previously added to the checkout to the cart with the cartCreate mutation. The problem that I'm facing is that the cart doesn't returns a checkout (id) so I have only the checkout URL, and I don't know if the user has completed the checkout and if the app should empty the cart once the web view is closed.

    How can I manage this situation?

    https://github.com/Shopify/mobile-buy-sdk-ios/issues/1181#tasklist-block-8e94a23b-e8d8-499f-886c-dde7943ef683

    opened by lorenzOliveto 3
  • vault payment and  apple pay payment issue

    vault payment and apple pay payment issue

    Hi Team

    I am facing issue with vault payment and apple pay payment - When i am doing vault payment then i am getting bellow error with Bogus Gatway - "There was an issue processing your payment. Try again or use a different payment method."

    When i am doing payment with apple pay then i am facing certificate issue which i have downloads from Shopify admin to create merchant certificate. Apple required RSA 2048 formate certificate but when i download CSR from shopify admin then it's coming RSA 256 formate. RSA 256 format apple is not allowing.

    Could you please help me in both payments? Looking forward your responce. Thanks

    opened by ankitbhandari27 1
  • ApplePay: SDK forces the user to enter shipping address when checking out with non-physical product

    ApplePay: SDK forces the user to enter shipping address when checking out with non-physical product

    I have a feeling that the Pay framework is very limited in terms of configuration & customisation. I am not sure if this is by design or some improvements have not been added yet.

    The question is: Is there a way to not force users to provide shipping address for non physical products?

    I think that this is the SDK limitation, there’s even a PR opened addressing that issue: https://github.com/Shopify/mobile-buy-sdk-ios/pull/1167/files#diff-826ed097f22711b75f0973ba8383ab130f1ea753987789c3fa43dd6763f40671R195

    Do you have plans addressing that issue? Or maybe this is due to some system requirements? How does Shopify backend calculate the taxes (based on customer / shipping address / billing address)?

    opened by bartosz-treeline 1
Releases(9.0.0)
  • 9.0.0(Oct 3, 2022)

  • 8.0.0(Jul 1, 2022)

  • 7.1.0(Apr 4, 2022)

  • 7.0.0(Jan 7, 2022)

  • 6.2.0(Oct 1, 2021)

  • 6.1.0(Jul 7, 2021)

  • 6.0.0(Apr 7, 2021)

  • 5.3.0(Jan 8, 2021)

  • 5.2.0(Oct 2, 2020)

  • 5.1.0(Sep 29, 2020)

  • 5.0.0(Aug 6, 2020)

  • 4.0.0(Apr 9, 2020)

  • 3.7.1(Apr 3, 2020)

  • 3.7.0(Jan 20, 2020)

    Release notes

    • Latest GraphQL schema - 2020-01
    • Support for FKP, GIP and SHP currencies
    • Support for presentmentUnitPrices, unitPrice and unitPriceMeasurement on ProductVariant
    • Support for rich media via new media field on Product:
      • External video
      • Images
      • 3D models
    Source code(tar.gz)
    Source code(zip)
  • 3.6.1(Oct 11, 2019)

  • 3.6.0(Jul 5, 2019)

    Release notes

    • Lock schema end-point to 2019-07
    • Add support for querying GraphQL schema version
    • Add requiresShipping field on ProductVariant
    • Add BAD_DOMAIN, GIFT_CARD_DEPLETED, INVALID_FOR_COUNTRY and TOTAL_PRICE_MISMATCH checkout error codes.
    • Deprecate CurrencyCode.byr
    Source code(tar.gz)
    Source code(zip)
  • 3.5.3(Jun 13, 2019)

    Release notes

    • Adds support for presentmentPriceRanges in Product
    • Adds support for presentmentAmountUsed in AppliedGiftCard
    • Updates deprecations in sample app
    Source code(tar.gz)
    Source code(zip)
  • 3.5.2(May 28, 2019)

  • 3.5.1(May 13, 2019)

  • 3.5.0(Apr 17, 2019)

  • 3.4.0(Apr 2, 2019)

    Release notes

    Multi Currency

    • Add enabledPresentmentCurrencies to payment settings.
    • Add amountUsedV2, balanceV2, paymentDueV2, subtotalPriceV2, totalPriceV2, totalTaxV2.
    • Deprecate amountUsed, balance, paymentDue, subtotalPrice, totalPrice, totalTax.

    Support

    • Add currency support for Panamian Balboa.
    • Add title to script discount application query.
    • Add lineItemsSubtotalPrice as a field returned on checkout.
    Source code(tar.gz)
    Source code(zip)
  • 3.3.3(Mar 18, 2019)

    Release notes

    • Adds currency support for Bermudian Dollar
    • Adds seo field to article which exposes SEO title and description information
    • Deprecates status field on transaction and replaces it with status_v2, which supports a null status.
    Source code(tar.gz)
    Source code(zip)
  • 3.3.2(Feb 13, 2019)

    Release notes

    • Adds checkoutUserErrors to several checkout-mutating fields
    • Adds support to query for products by availableForSale status
    • Fixes Carthage build failures
    Source code(tar.gz)
    Source code(zip)
  • 3.3.1(Jan 16, 2019)

  • 3.3(Dec 21, 2018)

    Release notes

    • Adds support for setting checkout line items directly.
    • Adds support for retrieving Page resources.
    • Adds presentmentPrices for product variants.
    • Add support for automatic discounts applications.
    • Add updated checkoutUserError fields, deprecates userError field.
    • Moves products and collections to live outside the shop scope.
    • Remove card vault deprecations.
    • Update sample app to remove deprecations.
    Source code(tar.gz)
    Source code(zip)
  • 3.2(Oct 31, 2018)

    Release notes

    • Add support for customer tags
    • Add support for discounts
    • Adds v2 Checkout-related mutations, deprecating previous versions
    • Adds improved support for customerUserError fields
    • Improvements to MoneyV2
    • Apple Pay support for discounts
    • Fixes ProxyView crash in sample app
    Source code(tar.gz)
    Source code(zip)
  • 3.1.9(Sep 21, 2018)

    Release notes

    • Updates SDK to Xcode 10 and Swift 4.2
    • Demos support for customer accounts in sample app
    • Demos support for associating customers to checkouts in sample app
    • Adds support for fetching articles by handle
    • Adds support for improved checkout attributes
    • Adds access token field to the customer reset mutation payload
    • Adds support for reseting customer by URL via universal links
    • Adds name field on Storefront.Order
    • Adds improved support for blogs and articles
    • Improves GraphQL field documentation
    Source code(tar.gz)
    Source code(zip)
  • 3.1.8(Aug 16, 2018)

    Release notes

    • Deprecates customer field in Storefront.Checkout.
    • Deprecates userErrors field in Storefront.CustomerAccessTokenCreatePayload in favour of the new Storefront.CustomerUserError that also provides additional machine-friendly error codes.
    • Adds ability to view fulfillment information for Storefront.Order, including tracking info, shipping companies, etc.
    • Adds mutation for appending multiple gift card codes.
    • Adds customer field in Storefront.CheckoutCustomerAssociatePayload.
    • Adds customerAccessToken in Storefront.CustomerActivatePayload.
    • Migrates away from CircleCI in favour of TravisCI.
    • Updates sample app to include support for gift cards.
    • Minor clean up, enhancements and documentation improvements.
    Source code(tar.gz)
    Source code(zip)
  • 3.1.7(Jun 13, 2018)

    Release Notes

    • Adds ability to remove an applied discount code from Checkout.
    • Adds Iraqi Dinar currency code.
    • Fixes an Apple Pay bug in the sample app that displayed an incorrect subtotal when item quantity was greater than one.
    • Optimization that prevents retrying cancelled network requests.
    Source code(tar.gz)
    Source code(zip)
  • 3.1.6(May 18, 2018)

    Release Notes

    • Adds handle for Article and Blog.
    • Adds a new country and currency code.
    • Deprecates MailingAddress.countryCode. Use countryCodeV2 instead.
    • Adds MoneyV2 entity.
    • Adds statusUrl for Order.
    • Adds ability to query for product variant's price range in Product using priceRange field.
    • Adds ability to check if a product is "in-stock" using the availableForSale field.
    • Adds shipsToCountries array on Shop to allow querying supported countries for shipping.
    • Miscellaneous under-the-hood enhancements and improvements
    Source code(tar.gz)
    Source code(zip)
Owner
Shopify
Shopify
Px-mobile-sdk-demo-app - PerimeterX Mobile SDK - Demo App

About PerimeterX PerimeterX is the leading provider of application security solu

PerimeterX 1 Nov 20, 2022
Twitter Kit is a native SDK to include Twitter content inside mobile apps.

Twitter will be discontinuing support for Twitter Kit on October 31, 2018. Read the blog post here. Twitter Kit for iOS Background Twitter Kit is a na

Twitter Archive 674 Dec 18, 2022
An Application to list the products and on selection of product list the detail of particular product

Merchandising An Application to list the products and on selection of product list the detail of particular product This application uses VIPER design

Poonam Yadav 0 Nov 28, 2021
Alter SDK is a cross-platform SDK consisting of a real-time 3D avatar system, facial motion capture, and an Avatar Designer component built from scratch for web3 interoperability and the open metaverse.

Alter SDK is a cross-platform SDK consisting of a real-time 3D avatar system, facial motion capture, and an Avatar Designer component built from scratch for web3 interoperability and the open metaverse.

Alter 45 Nov 29, 2022
iOS SDK for growing mobile in-app purchases

Adapty iOS SDK — in-app purchases start here Adapty SDK is an open-source framework that makes implementing in-app subscriptions in iOS fast and easy.

Adapty 339 Dec 14, 2022
Zendesk Mobile SDK for iOS

⚠️ This Repository has been deprecated, please go to here for the Zendesk Support SDK ⚠️ Zendesk Mobile SDK for iOS Zendesk SDK for mobile is a quick,

Zendesk 113 Dec 24, 2022
A simple to use iOS/tvOS/watchOS SDK to help get you off the ground quickly and efficiently with your Elastic Path Commerce Cloud written in Swift.

Elastic Path Commerce Cloud iOS Swift SDK A simple to use iOS/tvOS/watchOS SDK to help get you off the ground quickly and efficiently with your Elasti

Moltin 36 Aug 1, 2022
Home-assistant-swift-sdk - Used to integrate the Home Assistant APIs with your Swift-based apps.

home-assistant-swift-sdk This open-source library allows you to interact with a Home Assistant instance in your Swift-based (e.g., iOS, macOS, etc.) a

Alexander Golden 0 Dec 31, 2021
ResearchKit is an open source software framework that makes it easy to create apps for medical research or for other research projects.

ResearchKit Framework The ResearchKit™ framework is an open source software framework that makes it easy to create apps for medical research or for ot

null 5.5k Dec 26, 2022
Stacksift App SDK

Stacksift SDK Capture and submit crashes to Stacksift. This library ties together Wells and Impact to provide a full crash capturing and submission sy

Stacksift 44 Aug 18, 2022
Sample app to demonstrate the integration code and working of Dyte SDK for iOS, using Objective-C.

iOS sample app (using Objective-C) by dyte Sample app to demonstrate the usage of Dyte iOS SDK Explore the docs » View Demo · Report Bug · Request Fea

Dyte 8 Nov 26, 2021
Swiftui-webrtc-agora - SwiftUI webrtc app. use agora sdk for webrtc

Specs swiftui comnine firebase auth agora graphql apollo-ios( https://github.com

akiho 0 Jan 5, 2022
TelegramStickersImport — Telegram stickers importing SDK for iOS

TelegramStickersImport — Telegram stickers importing SDK for iOS TelegramStickersImport helps your users import third-party programaticaly created sti

null 35 Oct 26, 2022
RadioTimeKit - The Swift SDK for TuneIn RadioTimeKit is a Swift package to use the TuneIn API.

RadioTimeKit - The Swift SDK for TuneIn RadioTimeKit is a Swift package to use the TuneIn API. The goal for development was to have a Swift SDK to get

Frank Gregor 2 Jun 20, 2022
Official Appwrite SDK for Apple Devices 🍎

Appwrite Apple SDK This SDK is compatible with Appwrite server version 0.11.x. For older versions, please check previous releases. Appwrite is an open

Appwrite 55 Jan 2, 2023
Official Appwrite Swift SDK 🦅🍎

Appwrite Swift SDK This SDK is compatible with Appwrite server version 0.11.x. For older versions, please check previous releases. This is the Swift S

Appwrite 27 Dec 25, 2022
Muxer used on top of Feed iOS SDK for airplay

FeedAirplayMuxer Muxer used on top of Feed iOS SDK for airplay purposes. Demo Project --> https://github.com/feedfm/AirplayDemo Feed Airplay Muxer is

Feed Media 0 May 6, 2022
Basispay IOS SDK Version 2

BasisPay-IOS-KIT BasisPay IOS Payment Gateway kit for developers INTRODUCTION This document describes the steps for integrating Basispay online paymen

null 0 Oct 21, 2021
The Gini Bank SDK provides components for capturing, reviewing and analyzing photos of invoices and remittance slips.

Gini Bank SDK for iOS The Gini Bank SDK provides components for capturing, reviewing and analyzing photos of invoices and remittance slips. By integra

Gini GmbH 0 Dec 16, 2021