├── .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 | 
102 |
103 | ### Search products by tags
104 |
105 | ```javascript
106 |
107 | Shopify.getProducts(1, collectionId, ['t-shirts']).then(products => {});
108 |
109 | ```
110 |
111 | 
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 | 
124 |
125 | #### Proceed to checkout
126 |
127 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------