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