├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── reactnativeshopify │ ├── RNShopifyModule.java │ └── RNShopifyPackage.java ├── docs ├── add-to-cart.png ├── cart.png ├── customer-info.png ├── demo.gif ├── order-complete.png ├── payment-info.png ├── products-tag.png └── products.png ├── index.js ├── ios ├── RNShopify.h ├── RNShopify.m └── RNShopify.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ └── contents.xcworkspacedata └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "plugins": [ 7 | "react-native" 8 | ], 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "camelcase": 0, 12 | "import/no-extraneous-dependencies": 0, 13 | "import/extensions": 0, 14 | "import/prefer-default-export": 0, 15 | "quote-props": 0, 16 | "no-use-before-define": 0, 17 | "no-shadow": 0, 18 | "arrow-body-style": 0, 19 | "no-empty-label": 0, 20 | "no-console": 0, 21 | "import/no-unresolved": 0, 22 | "global-require": 0, 23 | "no-underscore-dangle": 0, 24 | "space-before-keywords": 0, 25 | "space-after-keywords": 0, 26 | "space-return-throw-case": 0, 27 | "react-native/no-unused-styles": 2, 28 | "react-native/split-platform-components": 2, 29 | "no-confusing-arrow": ["error", { "allowParens": true }] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Mac DS_Store 31 | .DS_Store 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | # Pods/ 40 | 41 | # Carthage 42 | # 43 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 44 | # Carthage/Checkouts 45 | 46 | Carthage/Build 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 54 | 55 | fastlane/report.xml 56 | fastlane/screenshots 57 | 58 | #Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Documentation 2 | docs 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-shopify 2 | 3 | ## Getting started 4 | 5 | `$ npm install react-native-shopify --save` 6 | 7 | ### Including Mobile-BUY-SDK 8 | 9 | Include the Shopify Mobile Buy SDK in your project to make it available to the bridge. 10 | Follow the instructions on their Github page to get started. For example, 11 | the recommended and easiest way for iOS is to install it as a Pod. This project will look 12 | for headers in the Pods directory. 13 | 14 | ### Mostly automatic installation 15 | 16 | `$ react-native link react-native-shopify` 17 | 18 | ### Manual installation 19 | 20 | 21 | #### iOS 22 | 23 | 1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` 24 | 2. Go to `node_modules` ➜ `react-native-shopify` and add `RNShopify.xcodeproj` 25 | 3. In XCode, in the project navigator, select your project. Add `libRNShopify.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 26 | 4. Run your project (`Cmd+R`)< 27 | 28 | #### Android 29 | 30 | 1. Open up `android/app/src/main/java/[...]/MainActivity.java` 31 | - Add `import com.reactnativeshopify.RNShopifyPackage;` to the imports at the top of the file 32 | - Add `new RNShopifyPackage()` to the list returned by the `getPackages()` method 33 | 2. Append the following lines to `android/settings.gradle`: 34 | ``` 35 | include ':react-native-shopify' 36 | project(':react-native-shopify').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-shopify/android') 37 | ``` 38 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 39 | ``` 40 | compile project(':react-native-shopify') 41 | ``` 42 | 43 | 44 | ## Usage 45 | 46 | ### Initialize the shop. 47 | 48 | ```javascript 49 | import Shopify from 'react-native-shopify'; 50 | 51 | Shopify.initialize('yourshopifystore.myshopify.com', 'YOUR API KEY'); 52 | 53 | ``` 54 | 55 | ### Fetch shop data, collections and tags 56 | 57 | ```javascript 58 | 59 | Shopify.getShop().then(shop => { 60 | // Save the shop somewhere and use it to display currency and other info 61 | return getAllCollections(); 62 | }).then(collections => { 63 | // Do something with collections 64 | return getAllTags(); 65 | }).then(tags => { 66 | // And tags... 67 | }); 68 | 69 | // You shoud load collections and tags from Shopify recursively since each query is 70 | // limited to 25 results by the SDK. Here are some methods to help you out: 71 | 72 | const getAllCollections = (page = 1, allCollections = []) => 73 | Shopify.getCollections(page).then((collections) => { 74 | if (_.size(collections)) { 75 | return getAllCollections(page + 1, [...allCollections, ...collections]); 76 | } 77 | return allCollections; 78 | }); 79 | 80 | // The same goes for tags... 81 | 82 | const getAllTags = (page = 1, allTags = []) => 83 | Shopify.getProductTags(page).then((tags) => { 84 | if (_.size(tags)) { 85 | return getAllTags(page + 1, [...allTags, ...tags]); 86 | } 87 | return allTags; 88 | }); 89 | 90 | // At last, fetch the first page (25) of products: 91 | 92 | Shopify.getProducts().then(products => { 93 | // Show products to your users 94 | }); 95 | 96 | // You can also fetch products for a specific page and collection ID 97 | 98 | Shopify.getProducts(2, collectionId).then(products => {}); 99 | 100 | ``` 101 | ![Products](docs/products.png) 102 | 103 | ### Search products by tags 104 | 105 | ```javascript 106 | 107 | Shopify.getProducts(1, collectionId, ['t-shirts']).then(products => {}); 108 | 109 | ``` 110 | 111 | ![Products by tag](docs/products-tag.png) 112 | 113 | ### Add products to cart and proceed to checkout 114 | 115 | A product has several variants. For example, a sweater in various sizes and colors. You add 116 | variants for products to the cart. A cart item is defined as a tuple of _item_, _variant_ and _quantity_. 117 | 118 | You can perform a native or web checkout. Contributions for Apple Pay are welcome! The steps below 119 | describe the native checkout flow. 120 | 121 | #### Add item to cart 122 | 123 | ![Add to cart](docs/add-to-cart.png) 124 | 125 | #### Proceed to checkout 126 | 127 | ![Cart](docs/cart.png) 128 | 129 | ```javascript 130 | 131 | // Add the first variant of the first fetched product, times 2: 132 | Shopify.getProducts().then(products => { 133 | const firstProduct = products[0]; 134 | 135 | // Note that you should set product and variant objects, not IDs. 136 | // Also note that the key for product is item 137 | const cartItem = { 138 | item: firstProduct, 139 | variant: firstProduct.variants[0], 140 | quantity: 2, 141 | }; 142 | 143 | const cart = [cartItem]; 144 | 145 | // Pass a clone of your cart because the SDK will mutate it 146 | Shopify.checkout(_.cloneDeep(cart)).then(() => { 147 | // We're ready to collect customer information 148 | }).catch((error) => { 149 | // You'll get a user friendly message here informing you exactly 150 | // what's wrong with the checkout and which items are not available. 151 | // The bridge parses native errors and constructs this message. 152 | Alert.alert( 153 | 'Error with checkout', 154 | error.message, 155 | ); 156 | }); 157 | }); 158 | 159 | ``` 160 | 161 | ### Collect customer information 162 | 163 | ![Customer information form](docs/customer-info.png) 164 | 165 | ```javascript 166 | 167 | const email = 'customer@mycustomer.com'; 168 | 169 | const addressInformation = { 170 | // Use the same properties as described in Shopify's iOS and Android SDK documentation 171 | }; 172 | 173 | Shopify.setCustomerInformation(email, addressInformation).then(() => { 174 | // Fetch shipping rates 175 | return Shopify.getShippingRates(); 176 | }).then((shippingRates) => { 177 | // Let the user choose a shipping rate 178 | // Select a shipping rate by index - 0 for the first rate: 179 | return Shopify.selectShippingRate(0); 180 | }).then(() => { 181 | // You're ready to collect payment information 182 | }); 183 | 184 | ``` 185 | 186 | ### Collect payment information 187 | 188 | ![Payment information form](docs/payment-info.png) 189 | 190 | ```javascript 191 | 192 | const creditCard = { 193 | // Use the same fields as in Shopify's SDK documentation 194 | // The only exception is that instead of nameOnCard you use firstName and lastName 195 | }; 196 | 197 | Shopify.completeCheckout({ ...creditCard }).then((order) => { 198 | // Congratulations, you got a new customer! 199 | // You get back the order object created from the successful checkout 200 | }); 201 | 202 | ``` 203 | 204 | ![Products](docs/order-complete.png) 205 | 206 | 207 | ### What can you do with it? 208 | 209 | You can browse through all products or filter them by collection and tag. You can call native checkout 210 | methods for both iOS and Android. We support web checkout for iOS but we have yet to implement the 211 | handlers for order completion so you can clear the cart or redirect the user to another page. 212 | 213 | We implemented custom parsing for checkout errors to give your users 214 | friendly messages on what went wrong. You can find out which line items are unavailable due to 215 | not enough quantity in stock and how many are remaining. You can also get feedback about which 216 | fields are invalid when entering customer and payment information. Feedback messages are available 217 | through the `message` property on the error object in checkout methods. 218 | 219 | You can find live code examples in the [Shoutem Shopify extension](https://github.com/shoutem/extensions/tree/master/shoutem-shopify), 220 | where you can also find UI components for various screens you might need. 221 | 222 | Here's a sample application in action: 223 | 224 | ![Products](docs/demo.gif) 225 | 226 | *All contributions are welcome!* 227 | 228 | These are the things missing: 229 | 230 | * Finishing web checkout on iOS and implementing it for Android 231 | * Apple Pay 232 | * Customer API 233 | 234 | We published a two part article series on bridging in React Native. Read it if you need more information about working with bridge libraries. 235 | The [first part](https://medium.com/shoutem/top-lessons-we-learned-while-building-a-react-native-bridge-library-bd6485cc6212) talks 236 | about high level concepts. The [second part](https://medium.com/shoutem/ways-to-pass-objects-between-native-and-javascript-in-react-native-c3dcae7bf4f5) 237 | goes into details about working with native objects. 238 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def safeExtGet(prop, fallback) { 4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 5 | } 6 | 7 | android { 8 | compileSdkVersion safeExtGet('compileSdkVersion', 23) 9 | buildToolsVersion safeExtGet('buildToolsVersion', "23.0.1") 10 | 11 | defaultConfig { 12 | minSdkVersion 16 13 | targetSdkVersion safeExtGet('targetSdkVersion', 22) 14 | versionCode 1 15 | versionName "1.0" 16 | ndk { 17 | abiFilters "armeabi-v7a", "x86" 18 | } 19 | } 20 | lintOptions { 21 | warning 'InvalidPackage' 22 | } 23 | } 24 | 25 | def googlePlayServicesVersion = safeExtGet('googlePlayServicesVersion', "12.0.0") 26 | def supportLibVersion = safeExtGet('supportLibVersion', "27.1.1") 27 | 28 | dependencies { 29 | compile 'com.facebook.react:react-native:0.20.+' 30 | 31 | implementation('com.shopify.mobilebuysdk:buy:2.1.0') { 32 | exclude group: 'com.google.android.gms', module: 'play-services-wallet' 33 | exclude group: 'com.android.support', module: 'support-v4' 34 | } 35 | 36 | compile "com.google.android.gms:play-services-wallet:${googlePlayServicesVersion}" 37 | compile "com.android.support:support-v4:${supportLibVersion}" 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeshopify/RNShopifyModule.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeshopify; 2 | 3 | import java.util.Date; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.Iterator; 8 | 9 | import org.json.*; 10 | 11 | import android.content.Context; 12 | import android.content.pm.ApplicationInfo; 13 | 14 | import com.facebook.react.bridge.ReactApplicationContext; 15 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 16 | import com.facebook.react.bridge.ReactMethod; 17 | import com.facebook.react.bridge.Promise; 18 | import com.facebook.react.bridge.ReadableArray; 19 | import com.facebook.react.bridge.ReadableMap; 20 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 21 | import com.facebook.react.bridge.ReadableType; 22 | import com.facebook.react.bridge.WritableMap; 23 | import com.facebook.react.bridge.WritableNativeMap; 24 | import com.facebook.react.bridge.WritableArray; 25 | import com.facebook.react.bridge.WritableNativeArray; 26 | import com.facebook.react.bridge.Arguments; 27 | 28 | import com.shopify.buy.dataprovider.*; 29 | import com.shopify.buy.model.*; 30 | 31 | public class RNShopifyModule extends ReactContextBaseJavaModule { 32 | 33 | private final ReactApplicationContext reactContext; 34 | private BuyClient buyClient; 35 | private Checkout checkout; 36 | private List availableShippingRates; 37 | 38 | public RNShopifyModule(ReactApplicationContext reactContext) { 39 | super(reactContext); 40 | this.reactContext = reactContext; 41 | } 42 | 43 | @Override 44 | public String getName() { 45 | return "RNShopify"; 46 | } 47 | 48 | @ReactMethod 49 | public void initialize(String domain, String key, final Promise promise) { 50 | //Application ID is always 8, as stated in official documentation from Shopify 51 | buyClient = new BuyClientBuilder() 52 | .shopDomain(domain) 53 | .apiKey(key) 54 | .appId("8") 55 | .applicationName(getApplicationName()) 56 | .build(); 57 | } 58 | 59 | @ReactMethod 60 | public void getShop(final Promise promise) { 61 | buyClient.getShop(new Callback() { 62 | @Override 63 | public void success(Shop shop) { 64 | try { 65 | promise.resolve(convertJsonToMap(new JSONObject(shop.toJsonString()))); 66 | } catch (JSONException e) { 67 | promise.reject("", e); 68 | } 69 | } 70 | 71 | @Override 72 | public void failure(BuyClientError error) { 73 | promise.reject("", error.getRetrofitErrorBody()); 74 | } 75 | }); 76 | } 77 | 78 | @ReactMethod 79 | public void getCollections(int page, final Promise promise) { 80 | buyClient.getCollections(page, new Callback>() { 81 | 82 | @Override 83 | public void success(List collections) { 84 | try { 85 | WritableArray array = new WritableNativeArray(); 86 | 87 | for(Collection collection : collections) { 88 | WritableMap collectionDictionary = convertJsonToMap(new JSONObject(collection.toJsonString())); 89 | collectionDictionary.putInt("id", collectionDictionary.getInt("collection_id")); 90 | array.pushMap(collectionDictionary); 91 | } 92 | 93 | promise.resolve(array); 94 | } catch (JSONException e) { 95 | promise.reject("", e); 96 | } 97 | } 98 | 99 | @Override 100 | public void failure(BuyClientError error) { 101 | promise.reject("", error.getRetrofitErrorBody()); 102 | } 103 | }); 104 | } 105 | 106 | @ReactMethod 107 | public void getProductTags(int page, final Promise promise) { 108 | buyClient.getProductTags(page, new Callback>() { 109 | 110 | @Override 111 | public void success(List tags) { 112 | WritableArray array = new WritableNativeArray(); 113 | 114 | for(String tag : tags) { 115 | array.pushString(tag); 116 | } 117 | 118 | promise.resolve(array); 119 | } 120 | 121 | @Override 122 | public void failure(BuyClientError error) { 123 | promise.reject("", error.getRetrofitErrorBody()); 124 | } 125 | }); 126 | } 127 | 128 | @ReactMethod 129 | public void getProductsPage(int page, final Promise promise) { 130 | buyClient.getProducts(page, new Callback>() { 131 | 132 | @Override 133 | public void success(List products) { 134 | try { 135 | promise.resolve(getProductsAsWritableArray(products)); 136 | } catch (JSONException e) { 137 | promise.reject("", e); 138 | } 139 | } 140 | 141 | @Override 142 | public void failure(BuyClientError error) { 143 | promise.reject("", error.getRetrofitErrorBody()); 144 | } 145 | }); 146 | } 147 | 148 | @ReactMethod 149 | public void getProductsWithTags(int page, ReadableArray tags, final Promise promise) { 150 | buyClient.getProducts(page, convertReadableArrayToSet(tags), new Callback>() { 151 | 152 | @Override 153 | public void success(List products) { 154 | try { 155 | promise.resolve(getProductsAsWritableArray(products)); 156 | } catch (JSONException e) { 157 | promise.reject("", e); 158 | } 159 | } 160 | 161 | @Override 162 | public void failure(BuyClientError error) { 163 | promise.reject("", error.getRetrofitErrorBody()); 164 | } 165 | }); 166 | } 167 | 168 | @ReactMethod 169 | public void getProductsWithTagsForCollection(int page, int collectionId, ReadableArray tags, 170 | final Promise promise) { 171 | Set tagSet = tags != null ? convertReadableArrayToSet(tags) : null; 172 | 173 | buyClient.getProducts(page, Long.valueOf(collectionId), tagSet, null, 174 | new Callback>() { 175 | 176 | @Override 177 | public void success(List products) { 178 | try { 179 | promise.resolve(getProductsAsWritableArray(products)); 180 | } catch (JSONException e) { 181 | promise.reject("", e); 182 | } 183 | } 184 | 185 | @Override 186 | public void failure(BuyClientError error) { 187 | promise.reject("", error.getRetrofitErrorBody()); 188 | } 189 | }); 190 | } 191 | 192 | @ReactMethod 193 | public void checkout(ReadableArray cartItems, final Promise promise) { 194 | Cart cart; 195 | 196 | try { 197 | cart = new Cart(); 198 | for (int i = 0; i < cartItems.size(); i++) { 199 | ReadableMap cartItem = cartItems.getMap(i); 200 | ReadableMap variantDictionary = cartItem.getMap("variant"); 201 | int quantity = cartItem.getInt("quantity"); 202 | 203 | JSONObject variantAsJsonObject = convertMapToJson(variantDictionary); 204 | ProductVariant variant = fromVariantJson(variantAsJsonObject.toString()); 205 | 206 | for(int j = 0; j < quantity; j++) { 207 | cart.addVariant(variant); 208 | } 209 | } 210 | } catch (JSONException e) { 211 | promise.reject("", e); 212 | return; 213 | } 214 | 215 | checkout = new Checkout(cart); 216 | 217 | // Sync the checkout with Shopify 218 | buyClient.createCheckout(checkout, new Callback() { 219 | 220 | @Override 221 | public void success(Checkout checkout) { 222 | RNShopifyModule.this.checkout = checkout; 223 | promise.resolve(true); 224 | } 225 | 226 | @Override 227 | public void failure(BuyClientError error) { 228 | promise.reject("", error.getRetrofitErrorBody()); 229 | } 230 | }); 231 | } 232 | 233 | @ReactMethod 234 | public void setCustomerInformation(String email, ReadableMap addressDictionary, final Promise promise) { 235 | try { 236 | String addressAsJson = convertMapToJson(addressDictionary).toString(); 237 | Address address = fromAddressJson(addressAsJson); 238 | address.setLastName(addressDictionary.getString("lastName")); 239 | address.setCountryCode(addressDictionary.getString("countryCode")); 240 | checkout.setEmail(email); 241 | checkout.setShippingAddress(address); 242 | checkout.setBillingAddress(address); 243 | } catch (JSONException e) { 244 | promise.reject("", e); 245 | return; 246 | } 247 | 248 | buyClient.updateCheckout(checkout, new Callback() { 249 | 250 | @Override 251 | public void success(Checkout checkout) { 252 | RNShopifyModule.this.checkout = checkout; 253 | promise.resolve(true); 254 | } 255 | 256 | @Override 257 | public void failure(BuyClientError error) { 258 | promise.reject("", error.getRetrofitErrorBody()); 259 | } 260 | }); 261 | } 262 | 263 | @ReactMethod 264 | public void getShippingRates(final Promise promise) { 265 | buyClient.getShippingRates(checkout.getToken(), new Callback>() { 266 | 267 | @Override 268 | public void success(List shippingRates) { 269 | RNShopifyModule.this.availableShippingRates = shippingRates; 270 | try { 271 | promise.resolve(getShippingRatesAsWritableArray(shippingRates)); 272 | } catch (JSONException e) { 273 | promise.reject("", e); 274 | return; 275 | } 276 | } 277 | 278 | @Override 279 | public void failure(BuyClientError error) { 280 | promise.reject("", error.getRetrofitErrorBody()); 281 | } 282 | }); 283 | } 284 | 285 | @ReactMethod 286 | public void selectShippingRate(int shippingRateIndex, final Promise promise) { 287 | ShippingRate selectedShippingRate = availableShippingRates.get(shippingRateIndex); 288 | checkout.setShippingRate(selectedShippingRate); 289 | 290 | // Update the checkout with the shipping rate 291 | buyClient.updateCheckout(checkout, new Callback() { 292 | @Override 293 | public void success(Checkout checkout) { 294 | RNShopifyModule.this.checkout = checkout; 295 | promise.resolve(true); 296 | } 297 | 298 | @Override 299 | public void failure(BuyClientError error) { 300 | promise.reject("", error.getRetrofitErrorBody()); 301 | } 302 | }); 303 | } 304 | 305 | @ReactMethod 306 | public void completeCheckout(ReadableMap cardDictionary, final Promise promise) { 307 | CreditCard card = new CreditCard(); 308 | card.setNumber(cardDictionary.getString("number")); 309 | card.setFirstName(cardDictionary.getString("firstName")); 310 | card.setLastName(cardDictionary.getString("lastName")); 311 | card.setMonth(cardDictionary.getString("expiryMonth")); 312 | card.setYear(cardDictionary.getString("expiryYear")); 313 | card.setVerificationValue(cardDictionary.getString("cvv")); 314 | 315 | // Associate the credit card with the checkout 316 | buyClient.storeCreditCard(card, checkout, new Callback() { 317 | @Override 318 | public void success(PaymentToken paymentToken) { 319 | buyClient.completeCheckout(paymentToken, checkout.getToken(), new Callback() { 320 | @Override 321 | public void success(Checkout returnedCheckout) { 322 | try { 323 | promise.resolve(convertJsonToMap(new JSONObject(returnedCheckout.getOrder().toJsonString()))); 324 | } catch (JSONException e) { 325 | promise.reject("", e); 326 | return; 327 | } 328 | } 329 | 330 | @Override 331 | public void failure(BuyClientError error) { 332 | promise.reject("", error.getRetrofitErrorBody()); 333 | } 334 | }); 335 | } 336 | 337 | @Override 338 | public void failure(BuyClientError error) { 339 | promise.reject("", error.getRetrofitErrorBody()); 340 | } 341 | }); 342 | } 343 | 344 | private WritableArray getProductsAsWritableArray(List products) throws JSONException { 345 | WritableArray array = new WritableNativeArray(); 346 | 347 | for (Product product : products) { 348 | WritableMap productMap = convertJsonToMap(new JSONObject(product.toJsonString())); 349 | productMap.putString("minimum_price", product.getMinimumPrice()); 350 | array.pushMap(productMap); 351 | } 352 | 353 | return array; 354 | } 355 | 356 | private WritableArray getShippingRatesAsWritableArray(List shippingRates) throws JSONException { 357 | WritableArray result = new WritableNativeArray(); 358 | 359 | for (ShippingRate shippingRate : shippingRates) { 360 | WritableMap shippingRateMap = convertJsonToMap(new JSONObject(toJsonString(shippingRate))); 361 | 362 | if(shippingRate.getDeliveryRangeDates() != null) { 363 | WritableArray deliveryDatesInMiliseconds = new WritableNativeArray(); 364 | 365 | for(Date deliveryDate : shippingRate.getDeliveryRangeDates()) { 366 | deliveryDatesInMiliseconds.pushDouble(deliveryDate.getTime()); 367 | } 368 | shippingRateMap.putArray("deliveryDates", deliveryDatesInMiliseconds); 369 | } 370 | result.pushMap(shippingRateMap); 371 | } 372 | 373 | return result; 374 | } 375 | 376 | private Set convertReadableArrayToSet(ReadableArray array) { 377 | Set set = new HashSet(); 378 | 379 | for (int i = 0; i < array.size(); i++) { 380 | set.add(array.getString(i)); 381 | } 382 | 383 | return set; 384 | } 385 | 386 | private WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException { 387 | WritableMap map = new WritableNativeMap(); 388 | 389 | Iterator iterator = jsonObject.keys(); 390 | while (iterator.hasNext()) { 391 | String key = iterator.next(); 392 | Object value = jsonObject.get(key); 393 | if (value instanceof JSONObject) { 394 | map.putMap(key, convertJsonToMap((JSONObject) value)); 395 | } else if (value instanceof JSONArray) { 396 | map.putArray(key, convertJsonToArray((JSONArray) value)); 397 | if(("option_values").equals(key)) { 398 | map.putArray("options", convertJsonToArray((JSONArray) value)); 399 | } 400 | } else if (value instanceof Boolean) { 401 | map.putBoolean(key, (Boolean) value); 402 | } else if (value instanceof Integer) { 403 | map.putInt(key, (Integer) value); 404 | } else if (value instanceof Double) { 405 | map.putDouble(key, (Double) value); 406 | } else if (value instanceof String) { 407 | map.putString(key, (String) value); 408 | } else { 409 | map.putString(key, value.toString()); 410 | } 411 | } 412 | return map; 413 | } 414 | 415 | private WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException { 416 | WritableArray array = new WritableNativeArray(); 417 | 418 | for (int i = 0; i < jsonArray.length(); i++) { 419 | Object value = jsonArray.get(i); 420 | if (value instanceof JSONObject) { 421 | array.pushMap(convertJsonToMap((JSONObject) value)); 422 | } else if (value instanceof JSONArray) { 423 | array.pushArray(convertJsonToArray((JSONArray) value)); 424 | } else if (value instanceof Boolean) { 425 | array.pushBoolean((Boolean) value); 426 | } else if (value instanceof Integer) { 427 | array.pushInt((Integer) value); 428 | } else if (value instanceof Double) { 429 | array.pushDouble((Double) value); 430 | } else if (value instanceof String) { 431 | array.pushString((String) value); 432 | } else { 433 | array.pushString(value.toString()); 434 | } 435 | } 436 | return array; 437 | } 438 | 439 | private JSONObject convertMapToJson(ReadableMap readableMap) throws JSONException { 440 | JSONObject object = new JSONObject(); 441 | ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); 442 | while (iterator.hasNextKey()) { 443 | String key = iterator.nextKey(); 444 | switch (readableMap.getType(key)) { 445 | case Null: 446 | object.put(key, JSONObject.NULL); 447 | break; 448 | case Boolean: 449 | object.put(key, readableMap.getBoolean(key)); 450 | break; 451 | case Number: 452 | object.put(key, readableMap.getDouble(key)); 453 | break; 454 | case String: 455 | object.put(key, readableMap.getString(key)); 456 | break; 457 | case Map: 458 | object.put(key, convertMapToJson(readableMap.getMap(key))); 459 | break; 460 | case Array: 461 | object.put(key, convertArrayToJson(readableMap.getArray(key))); 462 | break; 463 | } 464 | } 465 | return object; 466 | } 467 | 468 | private JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException { 469 | 470 | JSONArray array = new JSONArray(); 471 | 472 | for (int i = 0; i < readableArray.size(); i++) { 473 | switch (readableArray.getType(i)) { 474 | case Null: 475 | break; 476 | case Boolean: 477 | array.put(readableArray.getBoolean(i)); 478 | break; 479 | case Number: 480 | array.put(readableArray.getDouble(i)); 481 | break; 482 | case String: 483 | array.put(readableArray.getString(i)); 484 | break; 485 | case Map: 486 | array.put(convertMapToJson(readableArray.getMap(i))); 487 | break; 488 | case Array: 489 | array.put(convertArrayToJson(readableArray.getArray(i))); 490 | break; 491 | } 492 | } 493 | return array; 494 | } 495 | 496 | private ProductVariant fromVariantJson(String json) { 497 | return BuyClientUtils.createDefaultGson().fromJson(json, ProductVariant.class); 498 | } 499 | 500 | private Address fromAddressJson(String json) { 501 | return BuyClientUtils.createDefaultGson().fromJson(json, Address.class); 502 | } 503 | 504 | private String toJsonString(Object object) { 505 | return BuyClientUtils.createDefaultGson().toJson(object); 506 | } 507 | 508 | private String getApplicationName() { 509 | Context context = this.reactContext.getApplicationContext(); 510 | ApplicationInfo applicationInfo = context.getApplicationInfo(); 511 | int stringId = applicationInfo.labelRes; 512 | return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId); 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeshopify/RNShopifyPackage.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeshopify; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.bridge.NativeModule; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.uimanager.ViewManager; 11 | import com.facebook.react.bridge.JavaScriptModule; 12 | 13 | public class RNShopifyPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNShopifyModule(reactContext)); 17 | } 18 | 19 | public List> createJSModules() { 20 | return Collections.emptyList(); 21 | } 22 | 23 | @Override 24 | public List createViewManagers(ReactApplicationContext reactContext) { 25 | return Collections.emptyList(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/add-to-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/add-to-cart.png -------------------------------------------------------------------------------- /docs/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/cart.png -------------------------------------------------------------------------------- /docs/customer-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/customer-info.png -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/demo.gif -------------------------------------------------------------------------------- /docs/order-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/order-complete.png -------------------------------------------------------------------------------- /docs/payment-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/payment-info.png -------------------------------------------------------------------------------- /docs/products-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/products-tag.png -------------------------------------------------------------------------------- /docs/products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoutem/react-native-shopify/09d9a628294090705616461917bb77408141fd64/docs/products.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | import _ from 'lodash'; 3 | 4 | const { RNShopify } = NativeModules; 5 | 6 | const UNKNOWN_ERROR = 'There was an unknown error. Please contact our customer support.'; 7 | 8 | export default { 9 | ...RNShopify, 10 | getProducts: (page = 1, collectionId, tags) => { 11 | if (collectionId) { 12 | return RNShopify.getProductsWithTagsForCollection(page, collectionId, tags); 13 | } 14 | return tags ? RNShopify.getProductsWithTags(page, tags) : RNShopify.getProductsPage(page); 15 | }, 16 | checkout: (cart) => { 17 | return RNShopify.checkout(cart) 18 | .catch((error) => { 19 | throw new Error(getCheckoutError(error.message, cart)); 20 | }); 21 | }, 22 | setCustomerInformation: (email, customer) => { 23 | return RNShopify.setCustomerInformation(email, customer) 24 | .catch((error) => { 25 | throw new Error(getCheckoutError(error.message)); 26 | }); 27 | }, 28 | completeCheckout: (creditCard) => { 29 | return RNShopify.completeCheckout(creditCard) 30 | .catch((error) => { 31 | throw new Error(getCheckoutError(error.message)); 32 | }); 33 | }, 34 | }; 35 | 36 | /** 37 | * Converts checkout errors to a user friendly message. Shopify doesn't have a standard error format. 38 | * It replicates the structure that was sent in the request so we need to parse it manually. 39 | * This issue was reported here: https://github.com/Shopify/mobile-buy-sdk-ios/issues/22 40 | * Here is an example: 41 | * 42 | * {"errors":{"checkout":{"email":[{"code":"invalid","message":"is invalid","options":{}}], 43 | * "shipping_address":{"country_code":[{"code":"not_supported","message":"is not supported", 44 | * "options":{}}]},"billing_address":{"country_code":[{"code":"not_supported", 45 | * "message":"is not supported","options":{}}]}}}} 46 | * 47 | * Although Shopify has started to build helper methods for error parsing, there is still work 48 | * to be done there. Also, they are different for iOS and Android SDK. 49 | * 50 | * This JavaScript method gives you a complete solution for catching all checkout errors for 51 | * both iOS and Android. It also transforms keys into user friendly labels, for example 'last_name' 52 | * to 'Last name'. 53 | * 54 | * If the checkout error body is empty, this method will return an unknown error message. 55 | * If the body can't be parsed, it'll return it as it is. 56 | */ 57 | const getCheckoutError = (errorBody, cart) => { 58 | if (!errorBody) { 59 | return UNKNOWN_ERROR; 60 | } 61 | 62 | let errorObject; 63 | 64 | try { 65 | errorObject = JSON.parse(errorBody); 66 | } catch (e) { 67 | return errorBody; 68 | } 69 | 70 | const checkoutErrors = errorObject.errors.checkout; 71 | // This method is only used for checkout erorrs 72 | if (!checkoutErrors) { 73 | return UNKNOWN_ERROR; 74 | } 75 | 76 | // We first check for line item errors, which can happen only when trying to checkout the cart 77 | if (checkoutErrors.line_items) { 78 | return getLineItemsErrorMessage(checkoutErrors.line_items, cart); 79 | } 80 | 81 | return getErrorMessageFromCheckoutObject(checkoutErrors); 82 | }; 83 | 84 | /** 85 | * Converts line item errors to a user friendly message. If some of the items are unavailable, 86 | * Shopify will send an array of these errors, in the same order as the cart items used to start 87 | * the checkout. For each error, the message will describe how many units of the selected variant 88 | * are remaining in the store. Those items that can be bought are represented with a null in the array. 89 | * Here is an example: 90 | * 91 | * {"errors":{"checkout":{"line_items":[null,{"quantity":[{"message":"Not enough items available. 92 | * Only 2 left.","options":{"remaining":2},"code":"not_enough_in_stock"}]}]}}} 93 | * 94 | * Since this array matches the cart in order, we can display how many items for a product 95 | * and variant are remaining for each cart item that is unavailable. For example, let's say we 96 | * tried to buy a Small Navy sweater and 3 Grey Medium Sweaters. Suppose that only 2 Grey Sweaters 97 | * are available. We'll get null for the Navy sweater and an error message for Grey ones. The resulting 98 | * message will be: "Sweater - Grey Medium: Not enough items available. Only 2 left." 99 | * 100 | * We only handle quantity errors since these are known and documented. 101 | * 102 | * Available inventory is considered sensitive data and is only available through the admin API on Shopify, 103 | * and not through the SDK. This is explained by the Shopify's team here: 104 | * https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/mobile-buy-sdk-how-can-i-obtain-the-quantity-of-a-product-279864 105 | * 106 | * The checkout error is the only way to tell the user how many items are available, otherwise app 107 | * developers could implement a limit in the cart interface. 108 | * 109 | */ 110 | const getLineItemsErrorMessage = (lineItemErrors, cart) => { 111 | let errorMessage = 'Some of the items are unavailable. \n\n'; 112 | 113 | _.forEach(cart, (cartItem, index) => { 114 | const lineItemError = lineItemErrors[index]; 115 | const { item, variant } = cartItem; 116 | 117 | if (lineItemError && lineItemError.quantity) { 118 | const quantityError = lineItemError.quantity[0]; 119 | errorMessage += `${item.title} - ${variant.title}: ${quantityError.message} \n\n`; 120 | } 121 | }); 122 | return errorMessage; 123 | }; 124 | 125 | /** 126 | * Converts checkout errors to a user friendly message. Some checkout errors have 1 level, 127 | * such as an email error, and some have 2, such as credit card errors for number, cvv or other fields. 128 | * This method calls itself recursively to collect all of these errors and prefix them with parent keys. 129 | */ 130 | const getErrorMessageFromCheckoutObject = (errorObject, prefix) => { 131 | let errorMessage = ''; 132 | _.forOwn(errorObject, (value, key) => { 133 | const keyLabel = getLabelForErrorKey(key); 134 | 135 | if (_.isArray(value) && _.size(value)) { 136 | errorMessage += `${prefix ? `${prefix} ` : ''}${keyLabel} ${value[0].message}. \n\n`; 137 | } else { 138 | errorMessage += getErrorMessageFromCheckoutObject(value, keyLabel); 139 | } 140 | }); 141 | return errorMessage; 142 | }; 143 | 144 | /** 145 | * Since Shopify errors have keys not meant for end users, we convert them to a 146 | * readable format. For example, if we have an error in this format: {"credit_card": 147 | * {"verification_value": [{"message": "is invalid."}]}}, we want to show it as: 148 | * "Credit card verification value is invalid." 149 | */ 150 | const getLabelForErrorKey = (key) => { 151 | const dictionary = { 152 | billing_address: 'Billing address', 153 | email: 'Email', 154 | credit_card: 'Credit card', 155 | country_code: 'country code', 156 | last_name: 'last name', 157 | payment_gateway: 'Payment gateway:', 158 | shipping_address: 'Shipping address', 159 | verification_value: 'verification value', 160 | }; 161 | 162 | return dictionary[key] || key; 163 | }; 164 | -------------------------------------------------------------------------------- /ios/RNShopify.h: -------------------------------------------------------------------------------- 1 | // import RCTBridgeModule 2 | #if __has_include() 3 | #import 4 | #elif __has_include("RCTBridgeModule.h") 5 | #import "RCTBridgeModule.h" 6 | #else 7 | #import "React/RCTBridgeModule.h" // Required when used as a Pod in a Swift project 8 | #endif 9 | 10 | #import "Buy.h" 11 | 12 | @interface RNShopify : UIViewController 13 | 14 | @property (nonatomic, strong) BUYClient *client; 15 | @property (nonatomic, strong) BUYCheckout *checkout; 16 | @property (nonatomic, strong) UIViewController *rootViewController; 17 | @property (nonatomic, strong) NSArray *availableShippingRates; 18 | 19 | @end 20 | 21 | -------------------------------------------------------------------------------- /ios/RNShopify.m: -------------------------------------------------------------------------------- 1 | #import "RNShopify.h" 2 | #import "Buy.h" 3 | 4 | @implementation RNShopify { 5 | RCTPromiseResolveBlock _resolve; 6 | RCTPromiseRejectBlock _reject; 7 | } 8 | 9 | + (BOOL)requiresMainQueueSetup 10 | { 11 | return YES; 12 | } 13 | 14 | - (dispatch_queue_t)methodQueue 15 | { 16 | return dispatch_get_main_queue(); 17 | } 18 | 19 | RCT_EXPORT_MODULE() 20 | 21 | RCT_EXPORT_METHOD(initialize:(NSString *)domain key:(NSString *)key) 22 | { 23 | //Application ID is always 8, as stated in official documentation from Shopify 24 | self.client = [[BUYClient alloc] initWithShopDomain:domain 25 | apiKey:key 26 | appId:@"8"]; 27 | } 28 | 29 | RCT_EXPORT_METHOD(getShop:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 30 | { 31 | [self.client getShop:^(BUYShop *shop, NSError *error) { 32 | if (error) { 33 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 34 | } 35 | 36 | resolve([shop JSONDictionary]); 37 | }]; 38 | } 39 | 40 | RCT_EXPORT_METHOD(getCollections:(NSUInteger)page resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 41 | { 42 | [self.client getCollectionsPage:page completion:^(NSArray *collections, NSUInteger page, BOOL reachedEnd, NSError *error) { 43 | if (error) { 44 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 45 | } 46 | 47 | resolve([self getDictionariesForCollections:collections]); 48 | }]; 49 | } 50 | 51 | RCT_EXPORT_METHOD(getProductTags:(NSUInteger)page resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 52 | { 53 | [self.client getProductTagsPage:page completion:^(NSArray *tags, NSUInteger page, BOOL reachedEnd, NSError *error) { 54 | if (error) { 55 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 56 | } 57 | 58 | resolve(tags); 59 | }]; 60 | } 61 | 62 | RCT_EXPORT_METHOD(getProductsPage:(NSUInteger)page resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 63 | { 64 | [self.client getProductsPage:page completion:^(NSArray *products, NSUInteger page, BOOL reachedEnd, NSError *error) { 65 | if (error) { 66 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 67 | } 68 | 69 | resolve([self getDictionariesForProducts:products]); 70 | }]; 71 | } 72 | 73 | RCT_EXPORT_METHOD(getProductsWithTags:(NSUInteger)page tags:(NSArray *)tags resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 74 | { 75 | [self.client getProductsByTags:tags page:page completion:^(NSArray *products, NSError *error) { 76 | if (error) { 77 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 78 | } 79 | 80 | resolve([self getDictionariesForProducts:products]); 81 | }]; 82 | } 83 | 84 | RCT_EXPORT_METHOD(getProductsWithTagsForCollection:(NSUInteger)page collectionId:(nonnull NSNumber *)collectionId tags:(NSArray *)tags resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 85 | { 86 | [self.client getProductsPage:page inCollection:collectionId withTags:tags sortOrder:BUYCollectionSortCollectionDefault completion:^(NSArray *products, NSUInteger page, BOOL reachedEnd, NSError *error) { 87 | if (error) { 88 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 89 | } 90 | 91 | resolve([self getDictionariesForProducts:products]); 92 | }]; 93 | } 94 | 95 | RCT_EXPORT_METHOD(webCheckout:(NSArray *)cart resolver:(RCTPromiseResolveBlock)resolve 96 | rejecter:(RCTPromiseRejectBlock)reject) 97 | { 98 | _resolve = resolve; 99 | _reject = reject; 100 | 101 | BUYCheckout *checkout = [self createCheckoutFromCart:cart]; 102 | 103 | [self.client createCheckout:checkout completion:^(BUYCheckout *checkout, NSError *error) { 104 | if (error) { 105 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 106 | } 107 | 108 | BUYWebCheckoutPaymentProvider *webPaymentProvider = [[BUYWebCheckoutPaymentProvider alloc] initWithClient:self.client]; 109 | webPaymentProvider.delegate = self; 110 | 111 | [webPaymentProvider startCheckout:checkout]; 112 | }]; 113 | } 114 | 115 | RCT_EXPORT_METHOD(checkout:(NSArray *)cart resolver:(RCTPromiseResolveBlock)resolve 116 | rejecter:(RCTPromiseRejectBlock)reject) 117 | { 118 | BUYCheckout *checkout = [self createCheckoutFromCart:cart]; 119 | 120 | [self.client createCheckout:checkout completion:^(BUYCheckout *checkout, NSError *error) { 121 | if (error) { 122 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], 123 | [self getJsonFromError:error], error); 124 | } 125 | 126 | self.checkout = checkout; 127 | resolve(@YES); 128 | }]; 129 | } 130 | 131 | RCT_EXPORT_METHOD(setCustomerInformation:(NSString *)email address:(NSDictionary *)addressDictionary 132 | resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 133 | { 134 | BUYAddress *address = [self.client.modelManager insertAddressWithJSONDictionary:addressDictionary]; 135 | self.checkout.shippingAddress = address; 136 | self.checkout.billingAddress = address; 137 | self.checkout.email = email; 138 | 139 | [self.client updateCheckout:self.checkout completion:^(BUYCheckout *checkout, NSError *error) { 140 | if (error) { 141 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], 142 | [self getJsonFromError:error], error); 143 | } 144 | 145 | self.checkout = checkout; 146 | resolve(@YES); 147 | }]; 148 | } 149 | 150 | RCT_EXPORT_METHOD(getShippingRates:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 151 | { 152 | [self.client getShippingRatesForCheckoutWithToken:self.checkout.token completion:^(NSArray *shippingRates, BUYStatus status, NSError *error) { 153 | if (error) { 154 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 155 | } 156 | 157 | self.availableShippingRates = shippingRates; 158 | 159 | NSMutableArray *result = [NSMutableArray array]; 160 | 161 | for (BUYShippingRate *shippingRate in shippingRates) { 162 | NSMutableDictionary *shippingRateDictionary = [[shippingRate JSONDictionary] mutableCopy]; 163 | 164 | if ([shippingRate.deliveryRange count]) { 165 | double firstDateInMiliseconds = [shippingRate.deliveryRange[0] timeIntervalSince1970] * 1000; 166 | double secondDateInMiliseconds = [[shippingRate.deliveryRange lastObject] timeIntervalSince1970] * 1000; 167 | 168 | NSMutableArray *deliveryRange = [NSMutableArray array]; 169 | [deliveryRange addObject:[NSNumber numberWithDouble:firstDateInMiliseconds]]; 170 | [deliveryRange addObject:[NSNumber numberWithDouble:secondDateInMiliseconds]]; 171 | 172 | shippingRateDictionary[@"deliveryRange"] = deliveryRange; 173 | } 174 | [result addObject: shippingRateDictionary]; 175 | } 176 | resolve(result); 177 | }]; 178 | } 179 | 180 | RCT_EXPORT_METHOD(selectShippingRate:(NSUInteger)shippingRateIndex resolver:(RCTPromiseResolveBlock)resolve 181 | rejecter:(RCTPromiseRejectBlock)reject) 182 | { 183 | self.checkout.shippingRate = self.availableShippingRates[shippingRateIndex]; 184 | 185 | [self.client updateCheckout:self.checkout completion:^(BUYCheckout *checkout, NSError *error) { 186 | if (error) { 187 | return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); 188 | } 189 | 190 | self.checkout = checkout; 191 | resolve(@YES); 192 | }]; 193 | } 194 | 195 | RCT_EXPORT_METHOD(completeCheckout:(NSDictionary *)cardDictionary resolver:(RCTPromiseResolveBlock)resolve 196 | rejecter:(RCTPromiseRejectBlock)reject) 197 | { 198 | BUYCreditCard *creditCard = [[BUYCreditCard alloc] init]; 199 | creditCard.number = cardDictionary[@"number"]; 200 | creditCard.expiryMonth = cardDictionary[@"expiryMonth"]; 201 | creditCard.expiryYear = cardDictionary[@"expiryYear"]; 202 | creditCard.cvv = cardDictionary[@"cvv"]; 203 | creditCard.nameOnCard = [NSString stringWithFormat:@"%@ %@", cardDictionary[@"firstName"], cardDictionary[@"lastName"]]; 204 | 205 | [self.client storeCreditCard:creditCard checkout:self.checkout completion:^(id token, NSError *error) { 206 | if (error) { 207 | return reject(@"", [self getJsonFromError:error], error); 208 | } 209 | 210 | [self.client completeCheckoutWithToken:self.checkout.token paymentToken:token completion:^(BUYCheckout *returnedCheckout, NSError *error) { 211 | if (error) { 212 | return reject(@"", [self getJsonFromError:error], error); 213 | } 214 | 215 | self.checkout = returnedCheckout; 216 | resolve([self.checkout.order JSONDictionary]); 217 | }]; 218 | }]; 219 | } 220 | 221 | #pragma mark - BUYPaymentProvider delegate implementation - 222 | 223 | - (void)paymentProvider:(id)provider wantsControllerPresented:(UIViewController *)controller 224 | { 225 | self.rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; 226 | [self.rootViewController presentViewController:controller animated:YES completion:nil]; 227 | } 228 | 229 | // TODO: This method is never called. 230 | // The issue has been reported to Shopify: https://github.com/Shopify/mobile-buy-sdk-ios/issues/480 231 | - (void)paymentProviderWantsControllerDismissed:(id )provider 232 | { 233 | [self.rootViewController dismissViewControllerAnimated:YES completion:nil]; 234 | } 235 | 236 | - (void)paymentProvider:(id)provider didFailCheckoutWithError:(NSError *)error 237 | { 238 | _reject(@"checkout failed", @"", error); 239 | } 240 | 241 | - (void)paymentProviderDidDismissCheckout:(id)provider 242 | { 243 | _reject(@"checkout dismissed", @"", nil); 244 | } 245 | 246 | // TODO: This method is never called. 247 | // The issue has been reported to Shopify: https://github.com/Shopify/mobile-buy-sdk-ios/issues/428 248 | - (void)paymentProvider:(id )provider didCompleteCheckout:(BUYCheckout *)checkout withStatus:(BUYStatus)status 249 | { 250 | if (status == BUYStatusComplete) { 251 | _resolve(@"Done!"); 252 | } 253 | else { 254 | // TODO: How to handle this case? The prerequisite to think about it is that the method is actually called 255 | _resolve(@"Completed checkout with unknown status"); 256 | } 257 | } 258 | 259 | #pragma mark - Helpers - 260 | 261 | /** 262 | * We need this method to generate collection dictionaries manually because the JSONDictionary method 263 | * from the SDK crashes in certain cases. The issue has been reported and closed. It won't be resolved 264 | * in the near future. Check this link for details: https://github.com/Shopify/mobile-buy-sdk-ios/issues/351 265 | */ 266 | - (NSArray *) getDictionariesForCollections:(NSArray *)collections 267 | { 268 | NSMutableArray *result = [NSMutableArray array]; 269 | for (BUYCollection *collection in collections) { 270 | [result addObject: @{@"title":collection.title, @"id":collection.identifier}]; 271 | } 272 | return result; 273 | } 274 | 275 | /** 276 | * We need this method to add options for variants manually since the SDK's JSONDictionary method 277 | * doesn't return them 278 | */ 279 | - (NSArray *) getDictionariesForProducts:(NSArray *)products 280 | { 281 | NSMutableArray *result = [NSMutableArray array]; 282 | for (BUYProduct *product in products) { 283 | NSMutableDictionary *productDictionary = [[product JSONDictionary] mutableCopy]; 284 | 285 | NSMutableArray *variants = [NSMutableArray array]; 286 | 287 | for (BUYProductVariant *variant in product.variants) { 288 | NSMutableDictionary *variantDictionary = [[variant JSONDictionary] mutableCopy]; 289 | 290 | NSMutableArray *options = [NSMutableArray array]; 291 | 292 | for (BUYOptionValue *option in variant.options) { 293 | [options addObject: [option JSONDictionary]]; 294 | } 295 | 296 | variantDictionary[@"options"] = options; 297 | 298 | [variants addObject: variantDictionary]; 299 | } 300 | 301 | productDictionary[@"variants"] = variants; 302 | 303 | [result addObject: productDictionary]; 304 | } 305 | 306 | return result; 307 | } 308 | 309 | - (BUYCheckout *) createCheckoutFromCart:(NSArray *)cartItems 310 | { 311 | BUYModelManager *modelManager = self.client.modelManager; 312 | BUYCart *cart = [modelManager insertCartWithJSONDictionary:nil]; 313 | 314 | for (NSDictionary *cartItem in cartItems) { 315 | BUYProductVariant *variant = [[BUYProductVariant alloc] initWithModelManager:modelManager JSONDictionary:cartItem[@"variant"]]; 316 | for(int i = 0; i < [cartItem[@"quantity"] integerValue]; i++) { 317 | [cart addVariant:variant]; 318 | } 319 | } 320 | 321 | BUYCheckout *checkout = [modelManager checkoutWithCart:cart]; 322 | return checkout; 323 | } 324 | 325 | - (NSString *) getJsonFromError:(NSError *)error 326 | { 327 | // If user info can't be parsed to JSON the dataWithJSONObject will throw an exception 328 | // In this case, we default to localized description 329 | if(![NSJSONSerialization isValidJSONObject:error.userInfo]){ 330 | return error.localizedDescription; 331 | } 332 | 333 | NSError * err; 334 | NSData * jsonData = [NSJSONSerialization dataWithJSONObject:error.userInfo options:0 error:&err]; 335 | return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 336 | } 337 | 338 | @end 339 | -------------------------------------------------------------------------------- /ios/RNShopify.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3E7B58A1CC2AC0600A0062D /* RNShopify.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNShopify.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 10; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libRNShopify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNShopify.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B3E7B5881CC2AC0600A0062D /* RNShopify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNShopify.h; sourceTree = ""; }; 28 | B3E7B5891CC2AC0600A0062D /* RNShopify.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNShopify.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libRNShopify.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | B3E7B5881CC2AC0600A0062D /* RNShopify.h */, 54 | B3E7B5891CC2AC0600A0062D /* RNShopify.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 58B511DA1A9E6C8500147676 /* RNShopify */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNShopify" */; 65 | buildPhases = ( 66 | 58B511D71A9E6C8500147676 /* Sources */, 67 | 58B511D81A9E6C8500147676 /* Frameworks */, 68 | 58B511D91A9E6C8500147676 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = RNShopify; 75 | productName = RCTDataManager; 76 | productReference = 134814201AA4EA6300B7C361 /* libRNShopify.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 58B511D31A9E6C8500147676 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0610; 86 | ORGANIZATIONNAME = Facebook; 87 | TargetAttributes = { 88 | 58B511DA1A9E6C8500147676 = { 89 | CreatedOnToolsVersion = 6.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNShopify" */; 94 | compatibilityVersion = "Xcode 3.2"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 58B511D21A9E6C8500147676; 101 | productRefGroup = 58B511D21A9E6C8500147676; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 58B511DA1A9E6C8500147676 /* RNShopify */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 58B511D71A9E6C8500147676 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | B3E7B58A1CC2AC0600A0062D /* RNShopify.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 58B511ED1A9E6C8500147676 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_CONSTANT_CONVERSION = YES; 132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 133 | CLANG_WARN_EMPTY_BODY = YES; 134 | CLANG_WARN_ENUM_CONVERSION = YES; 135 | CLANG_WARN_INT_CONVERSION = YES; 136 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 137 | CLANG_WARN_UNREACHABLE_CODE = YES; 138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 139 | COPY_PHASE_STRIP = NO; 140 | ENABLE_STRICT_OBJC_MSGSEND = YES; 141 | GCC_C_LANGUAGE_STANDARD = gnu99; 142 | GCC_DYNAMIC_NO_PIC = NO; 143 | GCC_OPTIMIZATION_LEVEL = 0; 144 | GCC_PREPROCESSOR_DEFINITIONS = ( 145 | "DEBUG=1", 146 | "$(inherited)", 147 | ); 148 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 149 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 150 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 151 | GCC_WARN_UNDECLARED_SELECTOR = YES; 152 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 153 | GCC_WARN_UNUSED_FUNCTION = YES; 154 | GCC_WARN_UNUSED_VARIABLE = YES; 155 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 156 | MTL_ENABLE_DEBUG_INFO = YES; 157 | ONLY_ACTIVE_ARCH = YES; 158 | SDKROOT = iphoneos; 159 | }; 160 | name = Debug; 161 | }; 162 | 58B511EE1A9E6C8500147676 /* Release */ = { 163 | isa = XCBuildConfiguration; 164 | buildSettings = { 165 | ALWAYS_SEARCH_USER_PATHS = NO; 166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 167 | CLANG_CXX_LIBRARY = "libc++"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_WARN_BOOL_CONVERSION = YES; 171 | CLANG_WARN_CONSTANT_CONVERSION = YES; 172 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 173 | CLANG_WARN_EMPTY_BODY = YES; 174 | CLANG_WARN_ENUM_CONVERSION = YES; 175 | CLANG_WARN_INT_CONVERSION = YES; 176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 177 | CLANG_WARN_UNREACHABLE_CODE = YES; 178 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 179 | COPY_PHASE_STRIP = YES; 180 | ENABLE_NS_ASSERTIONS = NO; 181 | ENABLE_STRICT_OBJC_MSGSEND = YES; 182 | GCC_C_LANGUAGE_STANDARD = gnu99; 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 190 | MTL_ENABLE_DEBUG_INFO = NO; 191 | SDKROOT = iphoneos; 192 | VALIDATE_PRODUCT = YES; 193 | }; 194 | name = Release; 195 | }; 196 | 58B511F01A9E6C8500147676 /* Debug */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | HEADER_SEARCH_PATHS = ( 200 | "$(inherited)", 201 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 202 | "$(SRCROOT)/../../../React/**", 203 | "$(SRCROOT)/../../react-native/React/**", 204 | "$(SRCROOT)/../../../ios/Pods/**", 205 | ); 206 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 207 | OTHER_LDFLAGS = "-ObjC"; 208 | PRODUCT_NAME = RNShopify; 209 | SKIP_INSTALL = YES; 210 | }; 211 | name = Debug; 212 | }; 213 | 58B511F11A9E6C8500147676 /* Release */ = { 214 | isa = XCBuildConfiguration; 215 | buildSettings = { 216 | HEADER_SEARCH_PATHS = ( 217 | "$(inherited)", 218 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 219 | "$(SRCROOT)/../../../React/**", 220 | "$(SRCROOT)/../../react-native/React/**", 221 | "$(SRCROOT)/../../../ios/Pods/**", 222 | ); 223 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 224 | OTHER_LDFLAGS = "-ObjC"; 225 | PRODUCT_NAME = RNShopify; 226 | SKIP_INSTALL = YES; 227 | }; 228 | name = Release; 229 | }; 230 | /* End XCBuildConfiguration section */ 231 | 232 | /* Begin XCConfigurationList section */ 233 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNShopify" */ = { 234 | isa = XCConfigurationList; 235 | buildConfigurations = ( 236 | 58B511ED1A9E6C8500147676 /* Debug */, 237 | 58B511EE1A9E6C8500147676 /* Release */, 238 | ); 239 | defaultConfigurationIsVisible = 0; 240 | defaultConfigurationName = Release; 241 | }; 242 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNShopify" */ = { 243 | isa = XCConfigurationList; 244 | buildConfigurations = ( 245 | 58B511F01A9E6C8500147676 /* Debug */, 246 | 58B511F11A9E6C8500147676 /* Release */, 247 | ); 248 | defaultConfigurationIsVisible = 0; 249 | defaultConfigurationName = Release; 250 | }; 251 | /* End XCConfigurationList section */ 252 | }; 253 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 254 | } 255 | -------------------------------------------------------------------------------- /ios/RNShopify.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-shopify", 3 | "version": "0.2.7", 4 | "description": "React Native bridge for Shopify Mobile Buy SDK for iOS and Android", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react-native" 11 | ], 12 | "author": "Shoutem", 13 | "license": "BSD-3-Clause", 14 | "peerDependencies": { 15 | "react-native": ">=0.40.0" 16 | } 17 | } 18 | --------------------------------------------------------------------------------