1 package io.branch.rnbranch;
2 
3 import android.app.Activity;
4 import android.content.Context;
5 import android.content.Intent;
6 import android.content.IntentFilter;
7 import android.content.BroadcastReceiver;
8 import android.net.Uri;
9 import androidx.annotation.Nullable;
10 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
11 import android.util.Log;
12 import android.os.Handler;
13 
14 import com.facebook.react.bridge.*;
15 import com.facebook.react.bridge.Promise;
16 import com.facebook.react.modules.core.*;
17 import com.facebook.react.bridge.ReadableMap;
18 
19 import io.branch.referral.*;
20 import io.branch.referral.Branch.BranchLinkCreateListener;
21 import io.branch.referral.BuildConfig;
22 import io.branch.referral.util.*;
23 import io.branch.referral.Branch;
24 import io.branch.indexing.*;
25 
26 import org.json.*;
27 
28 import java.lang.ref.WeakReference;
29 import java.text.ParseException;
30 import java.text.SimpleDateFormat;
31 import java.util.*;
32 
33 public class RNBranchModule extends ReactContextBaseJavaModule {
34     public static final String REACT_CLASS = "RNBranch";
35     public static final String REACT_MODULE_NAME = "RNBranch";
36     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT = "io.branch.rnbranch.RNBranchModule.onInitSessionFinished";
37     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT_BRANCH_UNIVERSAL_OBJECT = "branch_universal_object";
38     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT_LINK_PROPERTIES = "link_properties";
39     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT_PARAMS = "params";
40     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT_ERROR = "error";
41     public static final String NATIVE_INIT_SESSION_FINISHED_EVENT_URI = "uri";
42     private static final String RN_INIT_SESSION_SUCCESS_EVENT = "RNBranch.initSessionSuccess";
43     private static final String RN_INIT_SESSION_ERROR_EVENT = "RNBranch.initSessionError";
44     private static final String INIT_SESSION_SUCCESS = "INIT_SESSION_SUCCESS";
45     private static final String INIT_SESSION_ERROR = "INIT_SESSION_ERROR";
46     private static final String ADD_TO_CART_EVENT = "ADD_TO_CART_EVENT";
47     private static final String ADD_TO_WISHLIST_EVENT = "ADD_TO_WISHLIST_EVENT";
48     private static final String PURCHASED_EVENT = "PURCHASED_EVENT";
49     private static final String PURCHASE_INITIATED_EVENT = "PURCHASE_INITIATED_EVENT";
50     private static final String REGISTER_VIEW_EVENT = "REGISTER_VIEW_EVENT";
51     private static final String SHARE_COMPLETED_EVENT = "SHARE_COMPLETED_EVENT";
52     private static final String SHARE_INITIATED_EVENT = "SHARE_INITIATED_EVENT";
53 
54     private static final String STANDARD_EVENT_ADD_TO_CART = "STANDARD_EVENT_ADD_TO_CART";
55     private static final String STANDARD_EVENT_ADD_TO_WISHLIST = "STANDARD_EVENT_ADD_TO_WISHLIST";
56     private static final String STANDARD_EVENT_VIEW_CART = "STANDARD_EVENT_VIEW_CART";
57     private static final String STANDARD_EVENT_INITIATE_PURCHASE = "STANDARD_EVENT_INITIATE_PURCHASE";
58     private static final String STANDARD_EVENT_ADD_PAYMENT_INFO = "STANDARD_EVENT_ADD_PAYMENT_INFO";
59     private static final String STANDARD_EVENT_PURCHASE = "STANDARD_EVENT_PURCHASE";
60     private static final String STANDARD_EVENT_SPEND_CREDITS = "STANDARD_EVENT_SPEND_CREDITS";
61 
62     private static final String STANDARD_EVENT_SEARCH = "STANDARD_EVENT_SEARCH";
63     private static final String STANDARD_EVENT_VIEW_ITEM = "STANDARD_EVENT_VIEW_ITEM";
64     private static final String STANDARD_EVENT_VIEW_ITEMS = "STANDARD_EVENT_VIEW_ITEMS";
65     private static final String STANDARD_EVENT_RATE = "STANDARD_EVENT_RATE";
66     private static final String STANDARD_EVENT_SHARE = "STANDARD_EVENT_SHARE";
67 
68     private static final String STANDARD_EVENT_COMPLETE_REGISTRATION = "STANDARD_EVENT_COMPLETE_REGISTRATION";
69     private static final String STANDARD_EVENT_COMPLETE_TUTORIAL = "STANDARD_EVENT_COMPLETE_TUTORIAL";
70     private static final String STANDARD_EVENT_ACHIEVE_LEVEL = "STANDARD_EVENT_ACHIEVE_LEVEL";
71     private static final String STANDARD_EVENT_UNLOCK_ACHIEVEMENT = "STANDARD_EVENT_UNLOCK_ACHIEVEMENT";
72 
73     private static final String IDENT_FIELD_NAME = "ident";
74     public static final String UNIVERSAL_OBJECT_NOT_FOUND_ERROR_CODE = "RNBranch::Error::BUONotFound";
75     private static final long AGING_HASH_TTL = 3600000;
76 
77     private static JSONObject initSessionResult = null;
78     private BroadcastReceiver mInitSessionEventReceiver = null;
79     private static Branch.BranchUniversalReferralInitListener initListener = null;
80 
81     private static Activity mActivity = null;
82     private static boolean mUseDebug = false;
83     private static boolean mInitialized = false;
84     private static JSONObject mRequestMetadata = new JSONObject();
85 
86     private AgingHash<String, BranchUniversalObject> mUniversalObjectMap = new AgingHash<>(AGING_HASH_TTL);
87 
88     public static void getAutoInstance(Context context) {
89         RNBranchConfig config = new RNBranchConfig(context);
90         String branchKey = config.getBranchKey();
91         String liveKey = config.getLiveKey();
92         String testKey = config.getTestKey();
93         boolean useTest = config.getUseTestInstance();
94 
95         if (branchKey != null) {
96             Branch.getAutoInstance(context, branchKey);
97         }
98         else if (useTest && testKey != null) {
99             Branch.getAutoInstance(context, testKey);
100         }
101         else if (!useTest && liveKey != null) {
102             Branch.getAutoInstance(context, liveKey);
103         }
104         else {
105             Branch.getAutoInstance(context);
106         }
107     }
108 
109     public static void initSession(final Uri uri, Activity reactActivity, Branch.BranchUniversalReferralInitListener anInitListener) {
110         initListener = anInitListener;
111         initSession(uri, reactActivity);
112     }
113 
114     public static void initSession(final Uri uri, Activity reactActivity) {
115         Branch branch = setupBranch(reactActivity.getApplicationContext());
116 
117         mActivity = reactActivity;
118         branch.initSession(new Branch.BranchReferralInitListener(){
119 
120             private Activity mmActivity = null;
121 
122             @Override
123             public void onInitFinished(JSONObject referringParams, BranchError error) {
124 
125                 Log.d(REACT_CLASS, "onInitFinished");
126                 JSONObject result = new JSONObject();
127                 Uri referringUri = null;
128                 try{
129                     boolean clickedBranchLink = false;
130                     // getXXX throws. It's OK for these to be missing.
131                     try {
132                         clickedBranchLink = referringParams.getBoolean("+clicked_branch_link");
133                     }
134                     catch (JSONException e) {
135 
136                     }
137 
138                     String referringLink = null;
139                     if (clickedBranchLink) {
140                         try {
141                             referringLink = referringParams.getString("~referring_link");
142                         }
143                         catch (JSONException e) {
144 
145                         }
146                     }
147                     else {
148                         try {
149                             referringLink = referringParams.getString("+non_branch_link");
150                         }
151                         catch (JSONException e) {
152 
153                         }
154                     }
155 
156                     if (referringLink != null) referringUri = Uri.parse(referringLink);
157 
158                     result.put(NATIVE_INIT_SESSION_FINISHED_EVENT_PARAMS, referringParams);
159                     result.put(NATIVE_INIT_SESSION_FINISHED_EVENT_ERROR, error != null ? error.getMessage() : JSONObject.NULL);
160                     result.put(NATIVE_INIT_SESSION_FINISHED_EVENT_URI, referringLink != null ? referringLink : JSONObject.NULL);
161                 } catch(JSONException ex) {
162                     try {
163                         result.put("error", "Failed to convert result to JSONObject: " + ex.getMessage());
164                     } catch(JSONException k) {}
165                 }
166                 initSessionResult = result;
167 
168                 BranchUniversalObject branchUniversalObject =  BranchUniversalObject.getReferredBranchUniversalObject();
169                 LinkProperties linkProperties = LinkProperties.getReferredLinkProperties();
170 
171                 if (initListener != null) {
172                     initListener.onInitFinished(branchUniversalObject, linkProperties, error);
173                 }
174                 generateLocalBroadcast(referringParams, referringUri, branchUniversalObject, linkProperties, error);
175             }
176 
177             private Branch.BranchReferralInitListener init(Activity activity) {
178                 mmActivity = activity;
179                 return this;
180             }
181 
182             private void generateLocalBroadcast(JSONObject referringParams,
183                                                 Uri uri,
184                                                 BranchUniversalObject branchUniversalObject,
185                                                 LinkProperties linkProperties,
186                                                 BranchError error) {
187                 Intent broadcastIntent = new Intent(NATIVE_INIT_SESSION_FINISHED_EVENT);
188 
189                 if (referringParams != null) {
190                     broadcastIntent.putExtra(NATIVE_INIT_SESSION_FINISHED_EVENT_PARAMS, referringParams.toString());
191                 }
192 
193                 if (branchUniversalObject != null) {
194                     broadcastIntent.putExtra(NATIVE_INIT_SESSION_FINISHED_EVENT_BRANCH_UNIVERSAL_OBJECT, branchUniversalObject);
195                 }
196 
197                 if (linkProperties != null) {
198                     broadcastIntent.putExtra(NATIVE_INIT_SESSION_FINISHED_EVENT_LINK_PROPERTIES, linkProperties);
199                 }
200 
201                 if (uri != null) {
202                     broadcastIntent.putExtra(NATIVE_INIT_SESSION_FINISHED_EVENT_URI, uri.toString());
203                 }
204 
205                 if (error != null) {
206                     broadcastIntent.putExtra(NATIVE_INIT_SESSION_FINISHED_EVENT_ERROR, error.getMessage());
207                 }
208 
209                 LocalBroadcastManager.getInstance(mmActivity).sendBroadcast(broadcastIntent);
210             }
211         }.init(reactActivity), uri, reactActivity);
212     }
213 
214     public static void setDebug() {
215         mUseDebug = true;
216     }
217 
218     public static void setRequestMetadata(String key, String val) {
219         if (key == null) {
220             return;
221         }
222 
223         if (mRequestMetadata.has(key) && val == null) {
224             mRequestMetadata.remove(key);
225         }
226 
227         try {
228             mRequestMetadata.put(key, val);
229         } catch (JSONException e) {
230             // no-op
231         }
232     }
233 
234     public RNBranchModule(ReactApplicationContext reactContext) {
235         super(reactContext);
236         forwardInitSessionFinishedEventToReactNative(reactContext);
237     }
238 
239     @javax.annotation.Nullable
240     @Override
241     public Map<String, Object> getConstants() {
242         final Map<String, Object> constants = new HashMap<>();
243         // RN events transmitted to JS
244 
245         constants.put(INIT_SESSION_SUCCESS, RN_INIT_SESSION_SUCCESS_EVENT);
246         constants.put(INIT_SESSION_ERROR, RN_INIT_SESSION_ERROR_EVENT);
247 
248         // Constants for use with userCompletedAction (deprecated)
249 
250         constants.put(ADD_TO_CART_EVENT, BranchEvent.ADD_TO_CART);
251         constants.put(ADD_TO_WISHLIST_EVENT, BranchEvent.ADD_TO_WISH_LIST);
252         constants.put(PURCHASED_EVENT, BranchEvent.PURCHASED);
253         constants.put(PURCHASE_INITIATED_EVENT, BranchEvent.PURCHASE_STARTED);
254         constants.put(REGISTER_VIEW_EVENT, BranchEvent.VIEW);
255         constants.put(SHARE_COMPLETED_EVENT, BranchEvent.SHARE_COMPLETED);
256         constants.put(SHARE_INITIATED_EVENT, BranchEvent.SHARE_STARTED);
257 
258         // constants for use with BranchEvent
259 
260         // Commerce events
261 
262         constants.put(STANDARD_EVENT_ADD_TO_CART, BRANCH_STANDARD_EVENT.ADD_TO_CART.getName());
263         constants.put(STANDARD_EVENT_ADD_TO_WISHLIST, BRANCH_STANDARD_EVENT.ADD_TO_WISHLIST.getName());
264         constants.put(STANDARD_EVENT_VIEW_CART, BRANCH_STANDARD_EVENT.VIEW_CART.getName());
265         constants.put(STANDARD_EVENT_INITIATE_PURCHASE, BRANCH_STANDARD_EVENT.INITIATE_PURCHASE.getName());
266         constants.put(STANDARD_EVENT_ADD_PAYMENT_INFO, BRANCH_STANDARD_EVENT.ADD_PAYMENT_INFO.getName());
267         constants.put(STANDARD_EVENT_PURCHASE, BRANCH_STANDARD_EVENT.PURCHASE.getName());
268         constants.put(STANDARD_EVENT_SPEND_CREDITS, BRANCH_STANDARD_EVENT.SPEND_CREDITS.getName());
269 
270         // Content Events
271 
272         constants.put(STANDARD_EVENT_SEARCH, BRANCH_STANDARD_EVENT.SEARCH.getName());
273         constants.put(STANDARD_EVENT_VIEW_ITEM, BRANCH_STANDARD_EVENT.VIEW_ITEM.getName());
274         constants.put(STANDARD_EVENT_VIEW_ITEMS , BRANCH_STANDARD_EVENT.VIEW_ITEMS.getName());
275         constants.put(STANDARD_EVENT_RATE, BRANCH_STANDARD_EVENT.RATE.getName());
276         constants.put(STANDARD_EVENT_SHARE, BRANCH_STANDARD_EVENT.SHARE.getName());
277 
278         // User Lifecycle Events
279 
280         constants.put(STANDARD_EVENT_COMPLETE_REGISTRATION, BRANCH_STANDARD_EVENT.COMPLETE_REGISTRATION.getName());
281         constants.put(STANDARD_EVENT_COMPLETE_TUTORIAL , BRANCH_STANDARD_EVENT.COMPLETE_TUTORIAL.getName());
282         constants.put(STANDARD_EVENT_ACHIEVE_LEVEL, BRANCH_STANDARD_EVENT.ACHIEVE_LEVEL.getName());
283         constants.put(STANDARD_EVENT_UNLOCK_ACHIEVEMENT, BRANCH_STANDARD_EVENT.UNLOCK_ACHIEVEMENT.getName());
284 
285         return constants;
286     }
287 
288     private void forwardInitSessionFinishedEventToReactNative(ReactApplicationContext reactContext) {
289         mInitSessionEventReceiver = new BroadcastReceiver() {
290             RNBranchModule mBranchModule;
291 
292             @Override
293             public void onReceive(Context context, Intent intent) {
294                 final boolean hasError = (initSessionResult.has("error") && !initSessionResult.isNull("error"));
295                 final String eventName = hasError ? RN_INIT_SESSION_ERROR_EVENT : RN_INIT_SESSION_SUCCESS_EVENT;
296                 mBranchModule.sendRNEvent(eventName, convertJsonToMap(initSessionResult));
297             }
298 
299             private BroadcastReceiver init(RNBranchModule branchModule) {
300                 mBranchModule = branchModule;
301                 return this;
302             }
303         }.init(this);
304 
305         LocalBroadcastManager.getInstance(reactContext).registerReceiver(mInitSessionEventReceiver, new IntentFilter(NATIVE_INIT_SESSION_FINISHED_EVENT));
306     }
307 
308     @Override
309     public void onCatalystInstanceDestroy() {
310         LocalBroadcastManager.getInstance(getReactApplicationContext()).unregisterReceiver(mInitSessionEventReceiver);
311     }
312 
313     @Override
314     public String getName() {
315         return REACT_MODULE_NAME;
316     }
317 
318     @ReactMethod
319     public void disableTracking(boolean disable) {
320         Branch branch = Branch.getInstance();
321         branch.disableTracking(disable);
322     }
323 
324     @ReactMethod
325     public void isTrackingDisabled(Promise promise) {
326         Branch branch = Branch.getInstance();
327         promise.resolve(branch.isTrackingDisabled());
328     }
329 
330     @ReactMethod
331     public void createUniversalObject(ReadableMap universalObjectMap, Promise promise) {
332         String ident = UUID.randomUUID().toString();
333         BranchUniversalObject universalObject = createBranchUniversalObject(universalObjectMap);
334         mUniversalObjectMap.put(ident, universalObject);
335 
336         WritableMap response = new WritableNativeMap();
337         response.putString(IDENT_FIELD_NAME, ident);
338         promise.resolve(response);
339     }
340 
341     @ReactMethod
342     public void releaseUniversalObject(String ident) {
343         mUniversalObjectMap.remove(ident);
344     }
345 
346     @ReactMethod
347     public void redeemInitSessionResult(Promise promise) {
348         promise.resolve(convertJsonToMap(initSessionResult));
349     }
350 
351     @ReactMethod
352     public void getLatestReferringParams(boolean synchronous, Promise promise) {
353         Branch branch = Branch.getInstance();
354         if (synchronous)
355             promise.resolve(convertJsonToMap(branch.getLatestReferringParamsSync()));
356         else
357             promise.resolve(convertJsonToMap(branch.getLatestReferringParams()));
358     }
359 
360     @ReactMethod
361     public void getFirstReferringParams(Promise promise) {
362         Branch branch = Branch.getInstance();
363         promise.resolve(convertJsonToMap(branch.getFirstReferringParams()));
364     }
365 
366     @ReactMethod
367     public void setIdentity(String identity) {
368         Branch branch = Branch.getInstance();
369         branch.setIdentity(identity);
370     }
371 
372     @ReactMethod
373     public void logout() {
374         Branch branch = Branch.getInstance();
375         branch.logout();
376     }
377 
378     @ReactMethod
379     public void logEvent(ReadableArray contentItems, String eventName, ReadableMap params, Promise promise) {
380         List<BranchUniversalObject> buos = new ArrayList<>();
381         for (int i = 0; i < contentItems.size(); ++ i) {
382             String ident = contentItems.getString(i);
383             BranchUniversalObject universalObject = findUniversalObjectOrReject(ident, promise);
384             if (universalObject == null) return;
385             buos.add(universalObject);
386         }
387 
388         BranchEvent event = createBranchEvent(eventName, params);
389         event.addContentItems(buos);
390         event.logEvent(mActivity);
391         promise.resolve(null);
392     }
393 
394     @ReactMethod
395     public void userCompletedAction(String event, ReadableMap appState) throws JSONException {
396         Branch branch = Branch.getInstance();
397         branch.userCompletedAction(event, convertMapToJson(appState));
398     }
399 
400     @ReactMethod
401     public void userCompletedActionOnUniversalObject(String ident, String event, ReadableMap state, Promise promise) {
402         BranchUniversalObject universalObject = findUniversalObjectOrReject(ident, promise);
403         if (universalObject == null) return;
404 
405         universalObject.userCompletedAction(event, convertMapToParams(state));
406         promise.resolve(null);
407     }
408 
409     @ReactMethod
410     public void sendCommerceEvent(String revenue, ReadableMap metadata, final Promise promise) throws JSONException {
411         Branch branch = Branch.getInstance();
412 
413         CommerceEvent commerceEvent = new CommerceEvent();
414         commerceEvent.setRevenue(Double.parseDouble(revenue));
415 
416         JSONObject jsonMetadata = null;
417         if (metadata != null) {
418             jsonMetadata = convertMapToJson(metadata);
419         }
420 
421         branch.sendCommerceEvent(commerceEvent, jsonMetadata, null);
422         promise.resolve(null);
423     }
424 
425     @ReactMethod
426     public void showShareSheet(String ident, ReadableMap shareOptionsMap, ReadableMap linkPropertiesMap, ReadableMap controlParamsMap, Promise promise) {
427         Context context = getReactApplicationContext();
428 
429         Handler mainHandler = new Handler(context.getMainLooper());
430 
431         Runnable myRunnable = new Runnable() {
432             Promise mPm;
433             Context mContext;
434             ReadableMap shareOptionsMap, linkPropertiesMap, controlParamsMap;
435             String ident;
436 
437             private Runnable init(ReadableMap _shareOptionsMap, String _ident, ReadableMap _linkPropertiesMap, ReadableMap _controlParamsMap, Promise promise, Context context) {
438                 mPm = promise;
439                 mContext = context;
440                 shareOptionsMap = _shareOptionsMap;
441                 ident = _ident;
442                 linkPropertiesMap = _linkPropertiesMap;
443                 controlParamsMap = _controlParamsMap;
444                 return this;
445             }
446 
447             @Override
448             public void run() {
449                 String messageHeader = shareOptionsMap.hasKey("messageHeader") ? shareOptionsMap.getString("messageHeader") : "";
450                 String messageBody = shareOptionsMap.hasKey("messageBody") ? shareOptionsMap.getString("messageBody") : "";
451                 ShareSheetStyle shareSheetStyle = new ShareSheetStyle(mContext, messageHeader, messageBody)
452                         .setCopyUrlStyle(mContext.getResources().getDrawable(android.R.drawable.ic_menu_send), "Copy", "Added to clipboard")
453                         .setMoreOptionStyle(mContext.getResources().getDrawable(android.R.drawable.ic_menu_search), "Show more")
454                         .addPreferredSharingOption(SharingHelper.SHARE_WITH.EMAIL)
455                         .addPreferredSharingOption(SharingHelper.SHARE_WITH.TWITTER)
456                         .addPreferredSharingOption(SharingHelper.SHARE_WITH.MESSAGE)
457                         .addPreferredSharingOption(SharingHelper.SHARE_WITH.FACEBOOK);
458 
459                 BranchUniversalObject branchUniversalObject = findUniversalObjectOrReject(ident, mPm);
460                 if (branchUniversalObject == null) {
461                     return;
462                 }
463 
464                 LinkProperties linkProperties = createLinkProperties(linkPropertiesMap, controlParamsMap);
465 
466                 branchUniversalObject.showShareSheet(
467                         getCurrentActivity(),
468                         linkProperties,
469                         shareSheetStyle,
470                         new Branch.BranchLinkShareListener() {
471                             private Promise mPromise = null;
472 
473                             @Override
474                             public void onShareLinkDialogLaunched() {
475                             }
476 
477                             @Override
478                             public void onShareLinkDialogDismissed() {
479                                 if(mPromise == null) {
480                                     return;
481                                 }
482 
483                                 WritableMap map = new WritableNativeMap();
484                                 map.putString("channel", null);
485                                 map.putBoolean("completed", false);
486                                 map.putString("error", null);
487                                 mPromise.resolve(map);
488                                 mPromise = null;
489                             }
490 
491                             @Override
492                             public void onLinkShareResponse(String sharedLink, String sharedChannel, BranchError error) {
493                                 if(mPromise == null) {
494                                     return;
495                                 }
496 
497                                 WritableMap map = new WritableNativeMap();
498                                 map.putString("channel", sharedChannel);
499                                 map.putBoolean("completed", true);
500                                 map.putString("error", (error != null ? error.getMessage() : null));
501                                 mPromise.resolve(map);
502                                 mPromise = null;
503                             }
504                             @Override
505                             public void onChannelSelected(String channelName) {
506                             }
507 
508                             private Branch.BranchLinkShareListener init(Promise promise) {
509                                 mPromise = promise;
510                                 return this;
511                             }
512                         }.init(mPm));
513             }
514         }.init(shareOptionsMap, ident, linkPropertiesMap, controlParamsMap, promise, context);
515 
516         mainHandler.post(myRunnable);
517     }
518 
519     @ReactMethod
520     public void registerView(String ident, Promise promise) {
521         BranchUniversalObject branchUniversalObject = findUniversalObjectOrReject(ident, promise);
522         if (branchUniversalObject == null) {
523              return;
524         }
525 
526         branchUniversalObject.registerView();
527         promise.resolve(null);
528     }
529 
530     @ReactMethod
531     public void generateShortUrl(String ident, ReadableMap linkPropertiesMap, ReadableMap controlParamsMap, final Promise promise) {
532         LinkProperties linkProperties = createLinkProperties(linkPropertiesMap, controlParamsMap);
533 
534         BranchUniversalObject branchUniversalObject = findUniversalObjectOrReject(ident, promise);
535         if (branchUniversalObject == null) {
536             return;
537         }
538 
539         branchUniversalObject.generateShortUrl(mActivity, linkProperties, new BranchLinkCreateListener() {
540             @Override
541             public void onLinkCreate(String url, BranchError error) {
542                 Log.d(REACT_CLASS, "onLinkCreate " + url);
543                 if (error != null) {
544                     if (error.getErrorCode() == BranchError.ERR_BRANCH_DUPLICATE_URL) {
545                         promise.reject("RNBranch::Error::DuplicateResourceError", error.getMessage());
546                     }
547                     else {
548                         promise.reject("RNBranch::Error", error.getMessage());
549                     }
550                     return;
551                 }
552 
553                 WritableMap map = new WritableNativeMap();
554                 map.putString("url", url);
555                 promise.resolve(map);
556             }
557         });
558     }
559 
560     @ReactMethod
561     public void openURL(String url, ReadableMap options) {
562         if (mActivity == null) {
563             // initSession is called before JS loads. This probably indicates failure to call initSession
564             // in an activity.
565             Log.e(REACT_CLASS, "Branch native Android SDK not initialized in openURL");
566             return;
567         }
568 
569         Intent intent = new Intent(mActivity, mActivity.getClass());
570         intent.putExtra("branch", url);
571         intent.putExtra("branch_force_new_session", true);
572 
573         if (options.hasKey("newActivity") && options.getBoolean("newActivity")) mActivity.finish();
574         mActivity.startActivity(intent);
575     }
576 
577     public static BranchEvent createBranchEvent(String eventName, ReadableMap params) {
578         BranchEvent event;
579         try {
580             BRANCH_STANDARD_EVENT standardEvent = BRANCH_STANDARD_EVENT.valueOf(eventName);
581             // valueOf on BRANCH_STANDARD_EVENT Enum has succeeded, so this is a standard event.
582             event = new BranchEvent(standardEvent);
583         } catch (IllegalArgumentException e) {
584             // The event name is not found in standard events.
585             // So use custom event mode.
586             event = new BranchEvent(eventName);
587         }
588 
589         if (params.hasKey("currency")) {
590             String currencyString = params.getString("currency");
591             CurrencyType currency = CurrencyType.getValue(currencyString);
592             if (currency != null) {
593                 event.setCurrency(currency);
594             }
595             else {
596                 Log.w(REACT_CLASS, "Invalid currency " + currencyString);
597             }
598         }
599 
600         if (params.hasKey("transactionID")) event.setTransactionID(params.getString("transactionID"));
601         if (params.hasKey("revenue")) event.setRevenue(Double.parseDouble(params.getString("revenue")));
602         if (params.hasKey("shipping")) event.setShipping(Double.parseDouble(params.getString("shipping")));
603         if (params.hasKey("tax")) event.setTax(Double.parseDouble(params.getString("tax")));
604         if (params.hasKey("coupon")) event.setCoupon(params.getString("coupon"));
605         if (params.hasKey("affiliation")) event.setAffiliation(params.getString("affiliation"));
606         if (params.hasKey("description")) event.setDescription(params.getString("description"));
607         if (params.hasKey("searchQuery")) event.setSearchQuery(params.getString("searchQuery"));
608 
609         if (params.hasKey("customData")) {
610             ReadableMap customData = params.getMap("customData");
611             ReadableMapKeySetIterator it = customData.keySetIterator();
612             while (it.hasNextKey()) {
613                 String key = it.nextKey();
614                 event.addCustomDataProperty(key, customData.getString(key));
615             }
616         }
617 
618         return event;
619     }
620 
621     public static LinkProperties createLinkProperties(ReadableMap linkPropertiesMap, @Nullable ReadableMap controlParams){
622         LinkProperties linkProperties = new LinkProperties();
623         if (linkPropertiesMap.hasKey("alias")) linkProperties.setAlias(linkPropertiesMap.getString("alias"));
624         if (linkPropertiesMap.hasKey("campaign")) linkProperties.setCampaign(linkPropertiesMap.getString("campaign"));
625         if (linkPropertiesMap.hasKey("channel")) linkProperties.setChannel(linkPropertiesMap.getString("channel"));
626         if (linkPropertiesMap.hasKey("feature")) linkProperties.setFeature(linkPropertiesMap.getString("feature"));
627         if (linkPropertiesMap.hasKey("stage")) linkProperties.setStage(linkPropertiesMap.getString("stage"));
628 
629         if (linkPropertiesMap.hasKey("tags")) {
630             ReadableArray tags = linkPropertiesMap.getArray("tags");
631             for (int i=0; i<tags.size(); ++i) {
632                 String tag = tags.getString(i);
633                 linkProperties.addTag(tag);
634             }
635         }
636 
637         if (controlParams != null) {
638             ReadableMapKeySetIterator iterator = controlParams.keySetIterator();
639             while (iterator.hasNextKey()) {
640                 String key = iterator.nextKey();
641                 Object value = getReadableMapObjectForKey(controlParams, key);
642                 linkProperties.addControlParameter(key, value.toString());
643             }
644         }
645 
646         return linkProperties;
647     }
648 
649     private static Branch setupBranch(Context context) {
650         Branch branch = Branch.getInstance(context);
651 
652         if (!mInitialized) {
653             Log.i(REACT_CLASS, "Initializing Branch SDK v. " + BuildConfig.VERSION_NAME);
654 
655             RNBranchConfig config = new RNBranchConfig(context);
656 
657             if (mUseDebug || config.getDebugMode()) branch.setDebug();
658 
659             if (mRequestMetadata != null) {
660                 Iterator keys = mRequestMetadata.keys();
661                 while (keys.hasNext()) {
662                     String key = (String) keys.next();
663                     try {
664                         branch.setRequestMetadata(key, mRequestMetadata.getString(key));
665                     } catch (JSONException e) {
666                         // no-op
667                     }
668                 }
669             }
670 
671             mInitialized = true;
672         }
673 
674         return branch;
675     }
676 
677     private BranchUniversalObject findUniversalObjectOrReject(final String ident, final Promise promise) {
678         BranchUniversalObject universalObject = mUniversalObjectMap.get(ident);
679 
680         if (universalObject == null) {
681             final String errorMessage = "BranchUniversalObject not found for ident " + ident + ".";
682             promise.reject(UNIVERSAL_OBJECT_NOT_FOUND_ERROR_CODE, errorMessage);
683         }
684 
685         return universalObject;
686     }
687 
688     public ContentMetadata createContentMetadata(ReadableMap map) {
689         ContentMetadata metadata = new ContentMetadata();
690 
691         if (map.hasKey("contentSchema")) {
692             BranchContentSchema schema = BranchContentSchema.valueOf(map.getString("contentSchema"));
693             metadata.setContentSchema(schema);
694         }
695 
696         if (map.hasKey("quantity")) {
697             metadata.setQuantity(map.getDouble("quantity"));
698         }
699 
700         if (map.hasKey("price")) {
701             double price = Double.parseDouble(map.getString("price"));
702             CurrencyType currency = null;
703             if (map.hasKey("currency")) currency = CurrencyType.valueOf(map.getString("currency"));
704             metadata.setPrice(price, currency);
705         }
706 
707         if (map.hasKey("sku")) {
708             metadata.setSku(map.getString("sku"));
709         }
710 
711         if (map.hasKey("productName")) {
712             metadata.setProductName(map.getString("productName"));
713         }
714 
715         if (map.hasKey("productBrand")) {
716             metadata.setProductBrand(map.getString("productBrand"));
717         }
718 
719         if (map.hasKey("productCategory")) {
720             ProductCategory category = getProductCategory(map.getString("productCategory"));
721             if (category != null) metadata.setProductCategory(category);
722         }
723 
724         if (map.hasKey("productVariant")) {
725             metadata.setProductVariant(map.getString("productVariant"));
726         }
727 
728         if (map.hasKey("condition")) {
729             ContentMetadata.CONDITION condition = ContentMetadata.CONDITION.valueOf(map.getString("condition"));
730             metadata.setProductCondition(condition);
731         }
732 
733         if (map.hasKey("ratingAverage") || map.hasKey("ratingMax") || map.hasKey("ratingCount")) {
734             Double average = null, max = null;
735             Integer count = null;
736             if (map.hasKey("ratingAverage")) average = map.getDouble("ratingAverage");
737             if (map.hasKey("ratingCount")) count = map.getInt("ratingCount");
738             if (map.hasKey("ratingMax")) max = map.getDouble("ratingMax");
739             metadata.setRating(average, max, count);
740         }
741 
742         if (map.hasKey("addressStreet") ||
743                 map.hasKey("addressCity") ||
744                 map.hasKey("addressRegion") ||
745                 map.hasKey("addressCountry") ||
746                 map.hasKey("addressPostalCode")) {
747             String street = null, city = null, region = null, country = null, postalCode = null;
748             if (map.hasKey("addressStreet")) street = map.getString("addressStreet");
749             if (map.hasKey("addressCity")) street = map.getString("addressCity");
750             if (map.hasKey("addressRegion")) street = map.getString("addressRegion");
751             if (map.hasKey("addressCountry")) street = map.getString("addressCountry");
752             if (map.hasKey("addressPostalCode")) street = map.getString("addressPostalCode");
753             metadata.setAddress(street, city, region, country, postalCode);
754         }
755 
756         if (map.hasKey("latitude") || map.hasKey("longitude")) {
757             Double latitude = null, longitude = null;
758             if (map.hasKey("latitude")) latitude = map.getDouble("latitude");
759             if (map.hasKey("longitude")) longitude = map.getDouble("longitude");
760             metadata.setLocation(latitude, longitude);
761         }
762 
763         if (map.hasKey("imageCaptions")) {
764             ReadableArray captions = map.getArray("imageCaptions");
765             for (int j=0; j < captions.size(); ++j) {
766                 metadata.addImageCaptions(captions.getString(j));
767             }
768         }
769 
770         if (map.hasKey("customMetadata")) {
771             ReadableMap customMetadata = map.getMap("customMetadata");
772             ReadableMapKeySetIterator it = customMetadata.keySetIterator();
773             while (it.hasNextKey()) {
774                 String key = it.nextKey();
775                 metadata.addCustomMetadata(key, customMetadata.getString(key));
776             }
777         }
778 
779         return metadata;
780     }
781 
782     public BranchUniversalObject createBranchUniversalObject(ReadableMap branchUniversalObjectMap) {
783         BranchUniversalObject branchUniversalObject = new BranchUniversalObject()
784                 .setCanonicalIdentifier(branchUniversalObjectMap.getString("canonicalIdentifier"));
785 
786         if (branchUniversalObjectMap.hasKey("title")) branchUniversalObject.setTitle(branchUniversalObjectMap.getString("title"));
787         if (branchUniversalObjectMap.hasKey("canonicalUrl")) branchUniversalObject.setCanonicalUrl(branchUniversalObjectMap.getString("canonicalUrl"));
788         if (branchUniversalObjectMap.hasKey("contentDescription")) branchUniversalObject.setContentDescription(branchUniversalObjectMap.getString("contentDescription"));
789         if (branchUniversalObjectMap.hasKey("contentImageUrl")) branchUniversalObject.setContentImageUrl(branchUniversalObjectMap.getString("contentImageUrl"));
790 
791         if (branchUniversalObjectMap.hasKey("locallyIndex")) {
792             if (branchUniversalObjectMap.getBoolean("locallyIndex")) {
793                 branchUniversalObject.setLocalIndexMode(BranchUniversalObject.CONTENT_INDEX_MODE.PUBLIC);
794             }
795             else {
796                 branchUniversalObject.setLocalIndexMode(BranchUniversalObject.CONTENT_INDEX_MODE.PRIVATE);
797             }
798         }
799 
800         if (branchUniversalObjectMap.hasKey("publiclyIndex")) {
801             if (branchUniversalObjectMap.getBoolean("publiclyIndex")) {
802                 branchUniversalObject.setContentIndexingMode(BranchUniversalObject.CONTENT_INDEX_MODE.PUBLIC);
803             }
804             else {
805                 branchUniversalObject.setContentIndexingMode(BranchUniversalObject.CONTENT_INDEX_MODE.PRIVATE);
806             }
807         }
808 
809         if (branchUniversalObjectMap.hasKey("contentIndexingMode")) {
810             switch (branchUniversalObjectMap.getType("contentIndexingMode")) {
811                 case String:
812                     String mode = branchUniversalObjectMap.getString("contentIndexingMode");
813 
814                     if (mode.equals("private"))
815                         branchUniversalObject.setContentIndexingMode(BranchUniversalObject.CONTENT_INDEX_MODE.PRIVATE);
816                     else if (mode.equals("public"))
817                         branchUniversalObject.setContentIndexingMode(BranchUniversalObject.CONTENT_INDEX_MODE.PUBLIC);
818                     else
819                         Log.w(REACT_CLASS, "Unsupported value for contentIndexingMode: " + mode +
820                                 ". Supported values are \"public\" and \"private\"");
821                     break;
822                 default:
823                     Log.w(REACT_CLASS, "contentIndexingMode must be a String");
824                     break;
825             }
826         }
827 
828         if (branchUniversalObjectMap.hasKey("currency") && branchUniversalObjectMap.hasKey("price")) {
829             String currencyString = branchUniversalObjectMap.getString("currency");
830             CurrencyType currency = CurrencyType.valueOf(currencyString);
831             branchUniversalObject.setPrice(branchUniversalObjectMap.getDouble("price"), currency);
832         }
833 
834         if (branchUniversalObjectMap.hasKey("expirationDate")) {
835             String expirationString = branchUniversalObjectMap.getString("expirationDate");
836             SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
837             format.setTimeZone(TimeZone.getTimeZone("UTC"));
838             try {
839                 Date date = format.parse(expirationString);
840                 Log.d(REACT_CLASS, "Expiration date is " + date.toString());
841                 branchUniversalObject.setContentExpiration(date);
842             }
843             catch (ParseException e) {
844                 Log.w(REACT_CLASS, "Invalid expiration date format. Valid format is YYYY-mm-ddTHH:MM:SS, e.g. 2017-02-01T00:00:00. All times UTC.");
845             }
846         }
847 
848         if (branchUniversalObjectMap.hasKey("keywords")) {
849             ReadableArray keywords = branchUniversalObjectMap.getArray("keywords");
850             for (int i=0; i<keywords.size(); ++i) {
851                 branchUniversalObject.addKeyWord(keywords.getString(i));
852             }
853         }
854 
855         if(branchUniversalObjectMap.hasKey("metadata")) {
856             ReadableMap metadataMap = branchUniversalObjectMap.getMap("metadata");
857             ReadableMapKeySetIterator iterator = metadataMap.keySetIterator();
858             while (iterator.hasNextKey()) {
859                 String metadataKey = iterator.nextKey();
860                 Object metadataObject = getReadableMapObjectForKey(metadataMap, metadataKey);
861                 branchUniversalObject.addContentMetadata(metadataKey, metadataObject.toString());
862                 HashMap<String, String> metadata = branchUniversalObject.getMetadata();
863             }
864         }
865 
866         if (branchUniversalObjectMap.hasKey("type")) branchUniversalObject.setContentType(branchUniversalObjectMap.getString("type"));
867 
868         if (branchUniversalObjectMap.hasKey("contentMetadata")) {
869             branchUniversalObject.setContentMetadata(createContentMetadata(branchUniversalObjectMap.getMap("contentMetadata")));
870         }
871 
872         return branchUniversalObject;
873     }
874 
875     @Nullable
876     public ProductCategory getProductCategory(final String stringValue) {
877         ProductCategory[] possibleValues = ProductCategory.class.getEnumConstants();
878         for (ProductCategory value: possibleValues) {
879             if (stringValue.equals(value.getName())) {
880                 return value;
881             }
882         }
883         Log.w(REACT_CLASS, "Could not find product category " + stringValue);
884         return null;
885     }
886 
887     @ReactMethod
888     public void redeemRewards(int value, String bucket, Promise promise)
889     {
890         if (bucket == null) {
891             Branch.getInstance().redeemRewards(value, new RedeemRewardsListener(promise));
892         } else {
893             Branch.getInstance().redeemRewards(bucket, value, new RedeemRewardsListener(promise));
894         }
895     }
896 
897     @ReactMethod
898     public void loadRewards(String bucket, Promise promise)
899     {
900         Branch.getInstance().loadRewards(new LoadRewardsListener(bucket, promise));
901     }
902 
903     @ReactMethod
904     public void getCreditHistory(Promise promise)
905     {
906         Branch.getInstance().getCreditHistory(new CreditHistoryListener(promise));
907     }
908 
909     protected class CreditHistoryListener implements Branch.BranchListResponseListener
910     {
911         private Promise _promise;
912 
913         // Constructor that takes in a required callbackContext object
914         public CreditHistoryListener(Promise promise) {
915             this._promise = promise;
916         }
917 
918         // Listener that implements BranchListResponseListener for getCreditHistory()
919         @Override
920         public void onReceivingResponse(JSONArray list, BranchError error) {
921             ArrayList<String> errors = new ArrayList<String>();
922             if (error == null) {
923                 try {
924                     ReadableArray result = convertJsonToArray(list);
925                     this._promise.resolve(result);
926                 } catch (JSONException err) {
927                     this._promise.reject(err.getMessage());
928                 }
929             } else {
930                 String errorMessage = error.getMessage();
931                 Log.d(REACT_CLASS, errorMessage);
932                 this._promise.reject(errorMessage);
933             }
934         }
935     }
936 
937     protected class RedeemRewardsListener implements Branch.BranchReferralStateChangedListener
938     {
939         private Promise _promise;
940 
941         public RedeemRewardsListener(Promise promise) {
942             this._promise = promise;
943         }
944 
945         @Override
946         public void onStateChanged(boolean changed, BranchError error) {
947             if (error == null) {
948                 WritableMap map = new WritableNativeMap();
949                 map.putBoolean("changed", changed);
950                 this._promise.resolve(map);
951             } else {
952                 String errorMessage = error.getMessage();
953                 Log.d(REACT_CLASS, errorMessage);
954                 this._promise.reject(errorMessage);
955             }
956         }
957     }
958 
959     protected class LoadRewardsListener implements Branch.BranchReferralStateChangedListener
960     {
961         private String _bucket;
962         private Promise _promise;
963 
964         public LoadRewardsListener(String bucket, Promise promise) {
965             this._bucket = bucket;
966             this._promise = promise;
967         }
968 
969         @Override
970         public void onStateChanged(boolean changed, BranchError error) {
971             if (error == null) {
972                 int credits = 0;
973                 if (this._bucket == null) {
974                   credits = Branch.getInstance().getCredits();
975                 } else {
976                   credits = Branch.getInstance().getCreditsForBucket(this._bucket);
977                 }
978                 WritableMap map = new WritableNativeMap();
979                 map.putInt("credits", credits);
980                 this._promise.resolve(map);
981             } else {
982                 String errorMessage = error.getMessage();
983                 Log.d(REACT_CLASS, errorMessage);
984                 this._promise.reject(errorMessage);
985             }
986         }
987     }
988 
989     public void sendRNEvent(String eventName, @Nullable WritableMap params) {
990         // This should avoid the crash in getJSModule() at startup
991         // See also: https://github.com/walmartreact/react-native-orientation-listener/issues/8
992 
993         ReactApplicationContext context = getReactApplicationContext();
994         Handler mainHandler = new Handler(context.getMainLooper());
995 
996         Runnable poller = new Runnable() {
997 
998             private Runnable init(ReactApplicationContext _context, Handler _mainHandler, String _eventName, WritableMap _params) {
999                 mMainHandler = _mainHandler;
1000                 mEventName = _eventName;
1001                 mContext = _context;
1002                 mParams = _params;
1003                 return this;
1004             }
1005 
1006             final int pollDelayInMs = 100;
1007             final int maxTries = 300;
1008 
1009             int tries = 1;
1010             String mEventName;
1011             WritableMap mParams;
1012             Handler mMainHandler;
1013             ReactApplicationContext mContext;
1014 
1015             @Override
1016             public void run() {
1017                 try {
1018                     Log.d(REACT_CLASS, "Catalyst instance poller try " + Integer.toString(tries));
1019                     if (mContext.hasActiveCatalystInstance()) {
1020                         Log.d(REACT_CLASS, "Catalyst instance active");
1021                         mContext
1022                                 .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
1023                                 .emit(mEventName, mParams);
1024                     } else {
1025                         tries++;
1026                         if (tries <= maxTries) {
1027                             mMainHandler.postDelayed(this, pollDelayInMs);
1028                         } else {
1029                             Log.e(REACT_CLASS, "Could not get Catalyst instance");
1030                         }
1031                     }
1032                 }
1033                 catch (Exception e) {
1034                     e.printStackTrace();
1035                 }
1036             }
1037         }.init(context, mainHandler, eventName, params);
1038 
1039         Log.d(REACT_CLASS, "sendRNEvent");
1040 
1041         mainHandler.post(poller);
1042     }
1043 
1044     private static Object getReadableMapObjectForKey(ReadableMap readableMap, String key) {
1045         switch (readableMap.getType(key)) {
1046             case Null:
1047                 return "Null";
1048             case Boolean:
1049                 return readableMap.getBoolean(key);
1050             case Number:
1051                 if (readableMap.getDouble(key) % 1 == 0) {
1052                     return readableMap.getInt(key);
1053                 } else {
1054                     return readableMap.getDouble(key);
1055                 }
1056             case String:
1057                 return readableMap.getString(key);
1058             default:
1059                 return "Unsupported Type";
1060         }
1061     }
1062 
1063     private static JSONObject convertMapToJson(ReadableMap readableMap) throws JSONException {
1064         JSONObject object = new JSONObject();
1065         ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
1066         while (iterator.hasNextKey()) {
1067             String key = iterator.nextKey();
1068             switch (readableMap.getType(key)) {
1069                 case Null:
1070                     object.put(key, JSONObject.NULL);
1071                     break;
1072                 case Boolean:
1073                     object.put(key, readableMap.getBoolean(key));
1074                     break;
1075                 case Number:
1076                     object.put(key, readableMap.getDouble(key));
1077                     break;
1078                 case String:
1079                     object.put(key, readableMap.getString(key));
1080                     break;
1081                 case Map:
1082                     object.put(key, convertMapToJson(readableMap.getMap(key)));
1083                     break;
1084                 case Array:
1085                     object.put(key, convertArrayToJson(readableMap.getArray(key)));
1086                     break;
1087             }
1088         }
1089         return object;
1090     }
1091 
1092     private static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException {
1093         JSONArray array = new JSONArray();
1094         for (int i = 0; i < readableArray.size(); i++) {
1095             switch (readableArray.getType(i)) {
1096                 case Null:
1097                     break;
1098                 case Boolean:
1099                     array.put(readableArray.getBoolean(i));
1100                     break;
1101                 case Number:
1102                     array.put(readableArray.getDouble(i));
1103                     break;
1104                 case String:
1105                     array.put(readableArray.getString(i));
1106                     break;
1107                 case Map:
1108                     array.put(convertMapToJson(readableArray.getMap(i)));
1109                     break;
1110                 case Array:
1111                     array.put(convertArrayToJson(readableArray.getArray(i)));
1112                     break;
1113             }
1114         }
1115         return array;
1116     }
1117 
1118     private static WritableMap convertJsonToMap(JSONObject jsonObject) {
1119         if(jsonObject == null) {
1120             return null;
1121         }
1122 
1123         WritableMap map = new WritableNativeMap();
1124 
1125         try {
1126             Iterator<String> iterator = jsonObject.keys();
1127             while (iterator.hasNext()) {
1128                 String key = iterator.next();
1129                 Object value = jsonObject.get(key);
1130                 if (value instanceof JSONObject) {
1131                     map.putMap(key, convertJsonToMap((JSONObject) value));
1132                 } else if (value instanceof  JSONArray) {
1133                     map.putArray(key, convertJsonToArray((JSONArray) value));
1134                 } else if (value instanceof  Boolean) {
1135                     map.putBoolean(key, (Boolean) value);
1136                 } else if (value instanceof  Integer) {
1137                     map.putInt(key, (Integer) value);
1138                 } else if (value instanceof  Double) {
1139                     map.putDouble(key, (Double) value);
1140                 } else if (value instanceof String)  {
1141                     map.putString(key, (String) value);
1142                 } else if (value == null || value == JSONObject.NULL) {
1143                     map.putNull(key);
1144                 } else {
1145                     map.putString(key, value.toString());
1146                 }
1147             }
1148         } catch(JSONException ex) {
1149             map.putString("error", "Failed to convert JSONObject to WriteableMap: " + ex.getMessage());
1150         }
1151 
1152         return map;
1153     }
1154 
1155     private static WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException {
1156         WritableArray array = new WritableNativeArray();
1157 
1158         for (int i = 0; i < jsonArray.length(); i++) {
1159             Object value = jsonArray.get(i);
1160             if (value instanceof JSONObject) {
1161                 array.pushMap(convertJsonToMap((JSONObject) value));
1162             } else if (value instanceof  JSONArray) {
1163                 array.pushArray(convertJsonToArray((JSONArray) value));
1164             } else if (value instanceof  Boolean) {
1165                 array.pushBoolean((Boolean) value);
1166             } else if (value instanceof  Integer) {
1167                 array.pushInt((Integer) value);
1168             } else if (value instanceof  Double) {
1169                 array.pushDouble((Double) value);
1170             } else if (value instanceof String)  {
1171                 array.pushString((String) value);
1172             } else {
1173                 array.pushString(value.toString());
1174             }
1175         }
1176         return array;
1177     }
1178 
1179     // Convert an arbitrary ReadableMap to a string-string hash of custom params for userCompletedAction.
1180     private static HashMap<String, String> convertMapToParams(ReadableMap map) {
1181         if (map == null) return null;
1182 
1183         HashMap<String, String> hash = new HashMap<>();
1184 
1185         ReadableMapKeySetIterator iterator = map.keySetIterator();
1186         while (iterator.hasNextKey()) {
1187             String key = iterator.nextKey();
1188             switch (map.getType(key)) {
1189                 case String:
1190                     hash.put(key, map.getString(key));
1191                 case Boolean:
1192                     hash.put(key, "" + map.getBoolean(key));
1193                 case Number:
1194                     hash.put(key, "" + map.getDouble(key));
1195                 default:
1196                     Log.w(REACT_CLASS, "Unsupported data type in params, ignoring");
1197             }
1198         }
1199 
1200         return hash;
1201     }
1202 }
1203