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