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