From 93f852fbbfc31a77f22a526f3803bb1b1ddb6251 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 18 May 2026 20:59:03 -0700 Subject: [PATCH 1/6] Phase 3: Align Android react-android dependency to RN 0.81.5 The libs.versions.toml had a TODO note about updating react-android in a dedicated work item; this is the right work item. - gradle/libs.versions.toml: react-android 0.79.3 -> 0.81.5 - libs/SalesforceReact/build.gradle.kts: removed the TODO comment Verified: ./gradlew :libs:SalesforceReact:assembleDebug succeeds. Note on remaining Android work for new architecture: the Java bridge modules (SalesforceOauthReactBridge, etc.) use @ReactMethod with separate success and error callbacks. Under new architecture interop mode, RN can wrap legacy @ReactMethod modules without code changes. Switching the test app to new architecture requires migrating its ReactNativeHost to DefaultReactNativeHost; that is best handled as a follow-up. --- gradle/libs.versions.toml | 2 +- libs/SalesforceReact/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27a993d939..eb3019e3cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ sqlcipher-android = "4.16.0" # Hybrid / React Native cordova-framework = "15.0.0" -react-android = "0.79.3" +react-android = "0.81.5" # Analytics deps tape = "1.2.3" diff --git a/libs/SalesforceReact/build.gradle.kts b/libs/SalesforceReact/build.gradle.kts index 2c89611b2d..80ced48efd 100644 --- a/libs/SalesforceReact/build.gradle.kts +++ b/libs/SalesforceReact/build.gradle.kts @@ -23,7 +23,7 @@ plugins { dependencies { api(project(":libs:MobileSync")) - api(libs.react.android) // TODO: This update should happen in a dedicated work item. ECJ20260423 + api(libs.react.android) implementation(libs.androidx.core.ktx) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.rules) From 36c8bee69304130f048e79c667a9d757a33d7ab0 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 22 May 2026 13:10:35 -0700 Subject: [PATCH 2/6] Phase 3: Convert Android bridges to Kotlin with TurboModule support - Convert all 4 bridge modules from Java to Kotlin - Add TurboModule interface to all bridges - Unify callback pattern: single callback(error, result) matching iOS - Update SalesforceReactActivity for single-callback pattern - Update ReactBridgeHelper with invokeSuccess/invokeError methods - Add kotlin jvmToolchain(17) to build.gradle.kts - Update architecture documentation --- docs/salesforcereact/ARCHITECTURE.md | 64 +- libs/SalesforceReact/build.gradle.kts | 4 + .../bridge/MobileSyncReactBridge.java | 272 ------- .../bridge/MobileSyncReactBridge.kt | 253 +++++++ .../reactnative/bridge/ReactBridgeHelper.java | 50 +- .../bridge/SalesforceNetReactBridge.java | 280 ------- .../bridge/SalesforceNetReactBridge.kt | 255 +++++++ .../bridge/SalesforceOauthReactBridge.java | 91 --- .../bridge/SalesforceOauthReactBridge.kt | 75 ++ .../bridge/SmartStoreReactBridge.java | 713 ------------------ .../bridge/SmartStoreReactBridge.kt | 526 +++++++++++++ .../ui/SalesforceReactActivity.java | 91 +-- 12 files changed, 1227 insertions(+), 1447 deletions(-) delete mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.java create mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.kt delete mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.java create mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.kt delete mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.java create mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.kt delete mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.java create mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.kt diff --git a/docs/salesforcereact/ARCHITECTURE.md b/docs/salesforcereact/ARCHITECTURE.md index ec19e11019..c2bf8ff4c2 100644 --- a/docs/salesforcereact/ARCHITECTURE.md +++ b/docs/salesforcereact/ARCHITECTURE.md @@ -804,32 +804,60 @@ public void onSuccess(RestRequest request, RestResponse response) { 3. **Async operations**: All I/O is asynchronous to prevent blocking 4. **Connection pooling**: RestClient reuses OkHttp connections -## Future Architecture Considerations +## New Architecture (TurboModules) -### New Architecture Migration +As of Mobile SDK 14.0, all Android bridge modules implement React Native's TurboModule interface for improved performance and type safety. -React Native's "New Architecture" introduces: -- **TurboModules**: Lazy-loaded native modules with type safety -- **Fabric**: New rendering system -- **JSI**: JavaScript Interface for direct JS ↔ Native communication +### Implementation -Migration would involve: -1. Converting `ReactContextBaseJavaModule` to `TurboModule` -2. Defining TypeScript specs for type safety -3. Replacing callback pattern with promises/async-await -4. Direct object passing instead of string serialization +All bridge classes: +- Extend `ReactContextBaseJavaModule` (backward compat) +- Implement `TurboModule` interface (new architecture) +- Written in Kotlin +- Use unified single-callback pattern matching iOS -### Codegen Specifications +```kotlin +class SalesforceOauthReactBridge(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { -Example TurboModule spec: -```typescript -export interface Spec extends TurboModule { - authenticate(): Promise; - getAuthCredentials(): Promise; - logout(): Promise; + @ReactMethod + fun getAuthCredentials(args: ReadableMap, callback: Callback) { + // callback(null, result) for success + // callback(errorMessage) for error + } } ``` +### Callback Pattern + +Both iOS and Android now use a unified single-callback pattern: +- **Success**: `callback.invoke(null, resultString)` +- **Error**: `callback.invoke(errorMessage)` + +The JavaScript `exec()` function handles both platforms identically: +```typescript +module[methodName](args, (error, result) => { + if (error) errorCB(safeJSONparse(error)); + else successCB(safeJSONparse(result)); +}); +``` + +### TypeScript Codegen Specs + +TurboModule specs are defined in `react-native-force/src/specs/`: +- `NativeSFOauthReactBridge.ts` +- `NativeSFNetReactBridge.ts` +- `NativeSFSmartStoreReactBridge.ts` +- `NativeSFMobileSyncReactBridge.ts` + +These specs enable React Native's Codegen to generate type-safe bindings. + +## Future Architecture Considerations + +### React Native Upgrade + +Before the 14.0 release, an upgrade to a React Native version supporting AGP 9+ is planned. This will eliminate the AGP compatibility patching currently done in template `installandroid.js` scripts. + ## Summary The SalesforceReact architecture provides a robust bridge between React Native and native Salesforce SDK functionality through: diff --git a/libs/SalesforceReact/build.gradle.kts b/libs/SalesforceReact/build.gradle.kts index 80ced48efd..25292cbb85 100644 --- a/libs/SalesforceReact/build.gradle.kts +++ b/libs/SalesforceReact/build.gradle.kts @@ -21,6 +21,10 @@ plugins { id("org.jetbrains.dokka") } +kotlin { + jvmToolchain(17) +} + dependencies { api(project(":libs:MobileSync")) api(libs.react.android) diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.java deleted file mode 100644 index dcab658e15..0000000000 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (c) 2015-present, salesforce.com, inc. - * All rights reserved. - * Redistribution and use of this software in source and binary forms, with or - * without modification, are permitted provided that the following conditions - * are met: - * - Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of salesforce.com, inc. nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission of salesforce.com, inc. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package com.salesforce.androidsdk.reactnative.bridge; - -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; -import com.salesforce.androidsdk.mobilesync.manager.SyncManager; -import com.salesforce.androidsdk.mobilesync.target.SyncDownTarget; -import com.salesforce.androidsdk.mobilesync.target.SyncUpTarget; -import com.salesforce.androidsdk.mobilesync.util.SyncOptions; -import com.salesforce.androidsdk.mobilesync.util.SyncState; -import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger; -import com.salesforce.androidsdk.smartstore.store.SmartStore; - -import org.json.JSONObject; - -public class MobileSyncReactBridge extends ReactContextBaseJavaModule { - - // Keys in json from/to javascript - static final String TARGET = "target"; - static final String SOUP_NAME = "soupName"; - static final String OPTIONS = "options"; - static final String SYNC_ID = "syncId"; - static final String SYNC_NAME = "syncName"; - public static final String TAG = "MobileSyncReactBridge"; - - public MobileSyncReactBridge(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public String getName() { - return TAG; - } - - /** - * Native implementation of syncUp - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void syncUp(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - // Parse args - JSONObject target = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(TARGET))); - String soupName = args.getString(SOUP_NAME); - JSONObject options = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(OPTIONS))); - String syncName = args.hasKey(SYNC_NAME) ? args.getString(SYNC_NAME) : null; - try { - final SyncManager syncManager = getSyncManager(args); - syncManager.syncUp(SyncUpTarget.fromJSON(target), SyncOptions.fromJSON(options), soupName, syncName, new SyncManager.SyncUpdateCallback() { - @Override - public void onUpdate(SyncState sync) { - handleSyncUpdate(sync, successCallback, errorCallback); - } - }); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "syncUp call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of syncDown - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void syncDown(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - // Parse args - JSONObject target = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(TARGET))); - String soupName = args.getString(SOUP_NAME); - JSONObject options = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(OPTIONS))); - String syncName = args.hasKey(SYNC_NAME) ? args.getString(SYNC_NAME) : null; - try { - final SyncManager syncManager = getSyncManager(args); - syncManager.syncDown(SyncDownTarget.fromJSON(target), SyncOptions.fromJSON(options), soupName, syncName, new SyncManager.SyncUpdateCallback() { - @Override - public void onUpdate(SyncState sync) { - handleSyncUpdate(sync, successCallback, errorCallback); - } - }); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "syncDown call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of getSyncStatus - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void getSyncStatus(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - try { - SyncState sync; - final SyncManager syncManager = getSyncManager(args); - if (args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID)) { - sync = syncManager.getSyncStatus(args.getInt(SYNC_ID)); - } - else if (args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME)) { - sync = syncManager.getSyncStatus(args.getString(SYNC_NAME)); - } - else { - throw new SyncManager.MobileSyncException("neither " + SYNC_ID + " nor " + SYNC_NAME + " were specified"); - } - ReactBridgeHelper.invoke(successCallback, sync == null ? null : sync.asJSON()); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "getSyncStatusByName call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of deleteSync - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void deleteSync(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - try { - final SyncManager syncManager = getSyncManager(args); - if (args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID)) { - syncManager.deleteSync(args.getInt(SYNC_ID)); - } - else if (args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME)) { - syncManager.deleteSync(args.getString(SYNC_NAME)); - } - else { - throw new SyncManager.MobileSyncException("neither " + SYNC_ID + " nor " + SYNC_NAME + " were specified"); - } - successCallback.invoke(); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "deleteSyncById call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of reSync - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void reSync(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - try { - final SyncManager syncManager = getSyncManager(args); - SyncManager.SyncUpdateCallback callback = new SyncManager.SyncUpdateCallback() { - @Override - public void onUpdate(SyncState sync) { - handleSyncUpdate(sync, successCallback, errorCallback); - } - }; - - if (args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID)) { - syncManager.reSync(args.getInt(SYNC_ID), callback); - } - else if (args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME)) { - syncManager.reSync(args.getString(SYNC_NAME), callback); - } - else { - throw new SyncManager.MobileSyncException("neither " + SYNC_ID + " nor " + SYNC_NAME + " were specified"); - } - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "reSync call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of cleanResyncGhosts - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void cleanResyncGhosts(ReadableMap args, - final Callback successCallback, final Callback errorCallback) { - // Parse args - long syncId = args.getInt(SYNC_ID); - try { - final SyncManager syncManager = getSyncManager(args); - syncManager.cleanResyncGhosts(syncId, new SyncManager.CleanResyncGhostsCallback() { - @Override - public void onSuccess(int numRecords) { - successCallback.invoke(numRecords); - } - - @Override - public void onError(Exception e) { - errorCallback.invoke(e.toString()); - } - }); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "cleanResyncGhosts call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Sync update handler - * @param sync - * @param errorCallback - */ - private void handleSyncUpdate(final SyncState sync, Callback successCallback, Callback errorCallback) { - try { - switch (sync.getStatus()) { - case NEW: - break; - case RUNNING: - break; - case DONE: - ReactBridgeHelper.invoke(successCallback, sync.asJSON()); - break; - case FAILED: - //Return sync to React Native with the error message in the JSON - ReactBridgeHelper.invoke(errorCallback, sync.asJSON()); - break; - } - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "handleSyncUpdate call failed", e); - } - } - - /** - * Return sync manager to use - * @param args Arguments passed to the bridge - * @return - */ - private SyncManager getSyncManager(ReadableMap args) throws Exception { - final SmartStore smartStore = SmartStoreReactBridge.getSmartStore(args); - final SyncManager syncManager = SyncManager.getInstance(null, null, smartStore); - return syncManager; - } -} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.kt b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.kt new file mode 100644 index 0000000000..04a84825c9 --- /dev/null +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/MobileSyncReactBridge.kt @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2015-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.reactnative.bridge + +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.turbomodule.core.interfaces.TurboModule +import com.salesforce.androidsdk.mobilesync.manager.SyncManager +import com.salesforce.androidsdk.mobilesync.target.SyncDownTarget +import com.salesforce.androidsdk.mobilesync.target.SyncUpTarget +import com.salesforce.androidsdk.mobilesync.util.SyncOptions +import com.salesforce.androidsdk.mobilesync.util.SyncState +import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger +import org.json.JSONObject + +class MobileSyncReactBridge(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { + + companion object { + // Keys in json from/to javascript + const val TARGET = "target" + const val SOUP_NAME = "soupName" + const val OPTIONS = "options" + const val SYNC_ID = "syncId" + const val SYNC_NAME = "syncName" + const val TAG = "MobileSyncReactBridge" + } + + override fun getName(): String = TAG + + /** + * Native implementation of syncUp + * @param args + * @param callback + */ + @ReactMethod + fun syncUp(args: ReadableMap, callback: Callback) { + // Parse args + val target = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(TARGET))) + val soupName = args.getString(SOUP_NAME)!! + val options = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(OPTIONS))) + val syncName = if (args.hasKey(SYNC_NAME)) args.getString(SYNC_NAME) else null + try { + val syncManager = getSyncManager(args) + syncManager.syncUp( + SyncUpTarget.fromJSON(target), + SyncOptions.fromJSON(options), + soupName, + syncName, + object : SyncManager.SyncUpdateCallback { + override fun onUpdate(sync: SyncState) { + handleSyncUpdate(sync, callback) + } + } + ) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "syncUp call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of syncDown + * @param args + * @param callback + */ + @ReactMethod + fun syncDown(args: ReadableMap, callback: Callback) { + // Parse args + val target = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(TARGET))) + val soupName = args.getString(SOUP_NAME)!! + val options = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(OPTIONS))) + val syncName = if (args.hasKey(SYNC_NAME)) args.getString(SYNC_NAME) else null + try { + val syncManager = getSyncManager(args) + syncManager.syncDown( + SyncDownTarget.fromJSON(target), + SyncOptions.fromJSON(options), + soupName, + syncName, + object : SyncManager.SyncUpdateCallback { + override fun onUpdate(sync: SyncState) { + handleSyncUpdate(sync, callback) + } + } + ) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "syncDown call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of getSyncStatus + * @param args + * @param callback + */ + @ReactMethod + fun getSyncStatus(args: ReadableMap, callback: Callback) { + try { + val syncManager = getSyncManager(args) + val sync = when { + args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID) -> + syncManager.getSyncStatus(args.getInt(SYNC_ID).toLong()) + args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME) -> + syncManager.getSyncStatus(args.getString(SYNC_NAME)) + else -> + throw SyncManager.MobileSyncException("neither $SYNC_ID nor $SYNC_NAME were specified") + } + ReactBridgeHelper.invokeSuccess(callback, sync?.asJSON()) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "getSyncStatusByName call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of deleteSync + * @param args + * @param callback + */ + @ReactMethod + fun deleteSync(args: ReadableMap, callback: Callback) { + try { + val syncManager = getSyncManager(args) + when { + args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID) -> + syncManager.deleteSync(args.getInt(SYNC_ID).toLong()) + args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME) -> + syncManager.deleteSync(args.getString(SYNC_NAME)) + else -> + throw SyncManager.MobileSyncException("neither $SYNC_ID nor $SYNC_NAME were specified") + } + ReactBridgeHelper.invokeSuccess(callback) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "deleteSyncById call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of reSync + * @param args + * @param callback + */ + @ReactMethod + fun reSync(args: ReadableMap, callback: Callback) { + try { + val syncManager = getSyncManager(args) + val syncUpdateCallback = object : SyncManager.SyncUpdateCallback { + override fun onUpdate(sync: SyncState) { + handleSyncUpdate(sync, callback) + } + } + when { + args.hasKey(SYNC_ID) && !args.isNull(SYNC_ID) -> + syncManager.reSync(args.getInt(SYNC_ID).toLong(), syncUpdateCallback) + args.hasKey(SYNC_NAME) && !args.isNull(SYNC_NAME) -> + syncManager.reSync(args.getString(SYNC_NAME)!!, syncUpdateCallback) + else -> + throw SyncManager.MobileSyncException("neither $SYNC_ID nor $SYNC_NAME were specified") + } + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "reSync call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of cleanResyncGhosts + * @param args + * @param callback + */ + @ReactMethod + fun cleanResyncGhosts(args: ReadableMap, callback: Callback) { + // Parse args + val syncId = args.getInt(SYNC_ID).toLong() + try { + val syncManager = getSyncManager(args) + syncManager.cleanResyncGhosts(syncId, object : SyncManager.CleanResyncGhostsCallback { + override fun onSuccess(numRecords: Int) { + ReactBridgeHelper.invokeSuccess(callback, numRecords) + } + + override fun onError(e: Exception?) { + ReactBridgeHelper.invokeError(callback, e?.toString() ?: "Unknown error") + } + }) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "cleanResyncGhosts call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Sync update handler + * @param sync + * @param callback + */ + private fun handleSyncUpdate(sync: SyncState, callback: Callback) { + try { + when (sync.status) { + SyncState.Status.NEW -> {} + SyncState.Status.RUNNING -> {} + SyncState.Status.DONE -> + ReactBridgeHelper.invokeSuccess(callback, sync.asJSON()) + SyncState.Status.FAILED -> + ReactBridgeHelper.invokeError(callback, sync.asJSON().toString()) + else -> {} + } + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "handleSyncUpdate call failed", e) + } + } + + /** + * Return sync manager to use + * @param args Arguments passed to the bridge + * @return + */ + @Throws(Exception::class) + private fun getSyncManager(args: ReadableMap): SyncManager { + val smartStore = SmartStoreReactBridge.getSmartStore(args) + return SyncManager.getInstance(null, null, smartStore) + } +} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/ReactBridgeHelper.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/ReactBridgeHelper.java index 4883cafa7f..01bfac3269 100644 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/ReactBridgeHelper.java +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/ReactBridgeHelper.java @@ -41,33 +41,42 @@ public class ReactBridgeHelper { - public static void invoke(Callback callback, JSONObject json) { - // XXX it would be better to user a NativeMap - // for now we serialize the object and do a JSON.parse(result) on the javascript side - callback.invoke(json == null ? null : json.toString()); + /** + * Invokes callback with success result using single-callback pattern: callback(null, result) + */ + public static void invokeSuccess(Callback callback, JSONObject json) { + callback.invoke(null, json == null ? null : json.toString()); } - public static void invoke(Callback callback, JSONArray json) { - // XXX it would be better to user a NativeArray - // for now we serialize the object and do a JSON.parse(result) on the javascript side - callback.invoke(json == null ? null : json.toString()); + public static void invokeSuccess(Callback callback, JSONArray json) { + callback.invoke(null, json == null ? null : json.toString()); } - public static void invoke(Callback callback, String value) { - // XXX we need to turn "xyz" into "\"xyz\"" so that JSON.parse() returns "xyz" - callback.invoke("\"" + value + "\""); + public static void invokeSuccess(Callback callback, String value) { + callback.invoke(null, "\"" + value + "\""); } - public static void invoke(Callback callback, boolean value) { - // XXX we need to turn true|false into "true"|"false" so that JSON.parse() returns true|false - callback.invoke("" + value); + public static void invokeSuccess(Callback callback, boolean value) { + callback.invoke(null, "" + value); } - public static void invoke(Callback callback, int value) { - // XXX we need to turn 123 into "123" so that JSON.parse() returns 123 - callback.invoke("" + value); + public static void invokeSuccess(Callback callback, int value) { + callback.invoke(null, "" + value); } + /** + * Invokes callback with no result (void success): callback(null) + */ + public static void invokeSuccess(Callback callback) { + callback.invoke(null, "null"); + } + + /** + * Invokes callback with error: callback(errorMessage) + */ + public static void invokeError(Callback callback, String error) { + callback.invoke(error); + } public static Map toJavaMap(ReadableMap map) { Map result = new HashMap<>(); @@ -82,7 +91,7 @@ public static Map toJavaMap(ReadableMap map) { result.put(key, map.getBoolean(key)); break; case Number: - result.put(key, map.getDouble(key)); // XXX what about integers + result.put(key, map.getDouble(key)); break; case String: result.put(key, map.getString(key)); @@ -108,7 +117,6 @@ public static Map toJavaStringStringMap(ReadableMap map) { result.put(key, map.getString(key)); break; default: - // Only expected strings break; } } @@ -125,7 +133,6 @@ public static Map> toJavaStringMapMap(ReadableMap map) result.put(key, toJavaStringStringMap(map.getMap(key))); break; default: - // Only expected maps break; } } @@ -141,7 +148,6 @@ public static List toJavaStringList(ReadableArray array) { result.add(i, array.getString(i)); break; default: - // Only expected strings break; } } @@ -159,7 +165,7 @@ public static List toJavaList(ReadableArray array) { result.add(i, array.getBoolean(i)); break; case Number: - result.add(i, array.getDouble(i)); // XXX what about integers + result.add(i, array.getDouble(i)); break; case String: result.add(i, array.getString(i)); diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.java deleted file mode 100644 index 91e9fa77dc..0000000000 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (c) 2015-present, salesforce.com, inc. - * All rights reserved. - * Redistribution and use of this software in source and binary forms, with or - * without modification, are permitted provided that the following conditions - * are met: - * - Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of salesforce.com, inc. nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission of salesforce.com, inc. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package com.salesforce.androidsdk.reactnative.bridge; - -import android.util.Base64; - -import androidx.annotation.NonNull; - -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; -import com.salesforce.androidsdk.reactnative.ui.SalesforceReactActivity; -import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger; -import com.salesforce.androidsdk.rest.RestClient; -import com.salesforce.androidsdk.rest.RestRequest; -import com.salesforce.androidsdk.rest.RestResponse; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.Map; - -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; - -public class SalesforceNetReactBridge extends ReactContextBaseJavaModule { - - private static final String METHOD_KEY = "method"; - private static final String END_POINT_KEY = "endPoint"; - private static final String PATH_KEY = "path"; - private static final String QUERY_PARAMS_KEY = "queryParams"; - private static final String HEADER_PARAMS_KEY = "headerParams"; - private static final String FILE_PARAMS_KEY = "fileParams"; - private static final String FILE_MIME_TYPE_KEY = "fileMimeType"; - private static final String FILE_URL_KEY = "fileUrl"; - private static final String FILE_NAME_KEY = "fileName"; - private static final String RETURN_BINARY = "returnBinary"; - private static final String ENCODED_BODY = "encodedBody"; - private static final String CONTENT_TYPE = "contentType"; - private static final String DOES_NOT_REQUIRE_AUTHENTICATION = "doesNotRequireAuthentication"; - private static final String TAG = "SalesforceNetReactBridge"; - - public SalesforceNetReactBridge(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public String getName() { - return TAG; - } - - @ReactMethod - public void sendRequest(ReadableMap args, final Callback successCallback, final Callback errorCallback) { - try { - // Prepare request - final RestRequest request = prepareRestRequest(args); - final boolean returnBinary = args.hasKey(RETURN_BINARY) && args.getBoolean(RETURN_BINARY); - final boolean doesNotRequireAuth = args.hasKey(DOES_NOT_REQUIRE_AUTHENTICATION) - && args.getBoolean(DOES_NOT_REQUIRE_AUTHENTICATION); - - // Sending request - final RestClient restClient = getRestClient(doesNotRequireAuth); - if (restClient == null) { - return; // we are detached - do nothing - } - restClient.sendAsync(request, new RestClient.AsyncRequestCallback() { - - @Override - public void onSuccess(RestRequest request, RestResponse response) { - try { - - // Sending a string over and letting javascript do a JSON.parse(result) - // It would be better to use NativeMap/NativeArray - // Although the absence of a common super class would force us to - // introduce two sendRequest methods: - // - one that expects map back from the server - // - one that expects array back from the server - - // Not a 2xx status - if (!response.isSuccess()) { - final JSONObject responseObject = new JSONObject(); - responseObject.put("headers", new JSONObject(response.getAllHeaders())); - responseObject.put("statusCode", response.getStatusCode()); - responseObject.put("body", parsedResponse(response)); - final JSONObject errorObject = new JSONObject(); - errorObject.put("response", responseObject); - errorCallback.invoke(errorObject.toString()); - } - - // Binary response - else if (returnBinary) { - JSONObject result = new JSONObject(); - result.put(CONTENT_TYPE, response.getContentType()); - result.put(ENCODED_BODY, Base64.encodeToString(response.asBytes(), Base64.DEFAULT)); - successCallback.invoke(result.toString()); - } - - // Other cases - else { - successCallback.invoke(response.asString()); - } - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "sendRequest failed", e); - onError(e); - } - } - - @Override - public void onError(Exception exception) { - final JSONObject errorObject = new JSONObject(); - try { - errorObject.put("error", exception.getMessage()); - } catch (JSONException jsonException) { - SalesforceReactLogger.e(TAG, "Error creating error object", jsonException); - } - errorCallback.invoke(errorObject.toString()); - } - }); - } catch (Exception exception) { - final JSONObject errorObject = new JSONObject(); - try { - errorObject.put("error", exception.getMessage()); - } catch (JSONException jsonException) { - SalesforceReactLogger.e(TAG, "Error creating error object", jsonException); - } - errorCallback.invoke(errorObject.toString()); - } - } - - private Object parsedResponse(RestResponse response) throws IOException { - // Is it a JSONObject? - final JSONObject responseAsJSONObject = parseResponseAsJSONObject(response); - if (responseAsJSONObject != null) { - return responseAsJSONObject; - } - - // Is it a JSONArray? - final JSONArray responseAsJSONArray = parseResponseAsJSONArray(response); - if (responseAsJSONArray != null) { - return responseAsJSONArray; - } - - // Otherwise return as string - return response.asString(); - } - - private JSONObject parseResponseAsJSONObject(RestResponse response) throws IOException { - try { - return response.asJSONObject(); - } - catch (JSONException e) { - // Not a JSON object - return null; - } - } - - private JSONArray parseResponseAsJSONArray(RestResponse response) throws IOException { - try { - return response.asJSONArray(); - } - catch (JSONException e) { - // Not a JSON array - return null; - } - } - - @NonNull - private RestRequest prepareRestRequest(ReadableMap args) throws UnsupportedEncodingException, URISyntaxException { - - // Parse args - RestRequest.RestMethod method = RestRequest.RestMethod.valueOf(args.getString(METHOD_KEY)); - String endPoint = !args.hasKey(END_POINT_KEY) || args.isNull(END_POINT_KEY) ? "" : args.getString(END_POINT_KEY); - String path = args.getString(PATH_KEY); - ReadableMap queryParams = args.getMap(QUERY_PARAMS_KEY); - ReadableMap headerParams = args.getMap(HEADER_PARAMS_KEY); - ReadableMap fileParams = args.getMap(FILE_PARAMS_KEY); - - // Preparing request - Map additionalHeaders = ReactBridgeHelper.toJavaStringStringMap(headerParams); - Map queryParamsMap = ReactBridgeHelper.toJavaMap(queryParams); - Map> fileParamsMap = ReactBridgeHelper.toJavaStringMapMap(fileParams); - String urlParams = ""; - RequestBody requestBody = null; - if (method == RestRequest.RestMethod.DELETE || method == RestRequest.RestMethod.GET - || method == RestRequest.RestMethod.HEAD) { - urlParams = buildQueryString(queryParamsMap); - } else { - requestBody = buildRequestBody(queryParamsMap, fileParamsMap); - } - String separator = urlParams.isEmpty() - ? "" - : path.contains("?") - ? (path.endsWith("&") ? "" : "&") - : "?"; - return new RestRequest(method, endPoint + path + separator + urlParams, requestBody, additionalHeaders); - } - - /** - * Returns the RestClient instance being used by this bridge. - * - * @param doesNotRequireAuth True - if an unauthenticated client should be used, False - otherwise. - * @return RestClient instance. - */ - protected RestClient getRestClient(boolean doesNotRequireAuth) { - final SalesforceReactActivity currentActivity = (SalesforceReactActivity) getCurrentActivity(); - if (currentActivity == null) { - return null; - } - if (doesNotRequireAuth) { - return currentActivity.buildClientManager().peekUnauthenticatedRestClient(); - } - return currentActivity.getRestClient(); - } - - private static String buildQueryString(Map params) throws UnsupportedEncodingException { - StringBuilder sb = new StringBuilder(); - for(Map.Entry entry : params.entrySet()) { - sb.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue().toString(), RestRequest.UTF_8)).append("&"); - } - return sb.toString(); - } - - private static RequestBody buildRequestBody(Map params, Map> fileParams) throws URISyntaxException { - final RequestBody paramsRequestBody = RequestBody.create(new JSONObject(params).toString(), RestRequest.MEDIA_TYPE_JSON); - if (fileParams.isEmpty()) { - return paramsRequestBody; - } else { - MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); - builder.addFormDataPart("", null, paramsRequestBody); - - // File params expected to be of the form: - // {: {fileMimeType:, fileUrl:, fileName:}} - for(Map.Entry> fileParamEntry : fileParams.entrySet()) { - Map fileParam = fileParamEntry.getValue(); - String fileParamName = fileParamEntry.getKey(); - String mimeType = fileParam.get(FILE_MIME_TYPE_KEY); - String name = fileParam.get(FILE_NAME_KEY); - URI url = new URI(fileParam.get(FILE_URL_KEY)); - File file = new File(url); - MediaType mediaType = MediaType.parse(mimeType); - builder.addFormDataPart(fileParamName, name, RequestBody.create(file, mediaType)); - } - return builder.build(); - } - } -} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.kt b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.kt new file mode 100644 index 0000000000..9548aa988f --- /dev/null +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceNetReactBridge.kt @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2015-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.reactnative.bridge + +import android.util.Base64 +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.turbomodule.core.interfaces.TurboModule +import com.salesforce.androidsdk.reactnative.ui.SalesforceReactActivity +import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger +import com.salesforce.androidsdk.rest.RestClient +import com.salesforce.androidsdk.rest.RestRequest +import com.salesforce.androidsdk.rest.RestResponse +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.IOException +import java.net.URI +import java.net.URLEncoder + +open class SalesforceNetReactBridge( + reactContext: ReactApplicationContext +) : ReactContextBaseJavaModule(reactContext), TurboModule { + + companion object { + private const val METHOD_KEY = "method" + private const val END_POINT_KEY = "endPoint" + private const val PATH_KEY = "path" + private const val QUERY_PARAMS_KEY = "queryParams" + private const val HEADER_PARAMS_KEY = "headerParams" + private const val FILE_PARAMS_KEY = "fileParams" + private const val FILE_MIME_TYPE_KEY = "fileMimeType" + private const val FILE_URL_KEY = "fileUrl" + private const val FILE_NAME_KEY = "fileName" + private const val RETURN_BINARY = "returnBinary" + private const val ENCODED_BODY = "encodedBody" + private const val CONTENT_TYPE = "contentType" + private const val DOES_NOT_REQUIRE_AUTHENTICATION = "doesNotRequireAuthentication" + private const val TAG = "SalesforceNetReactBridge" + } + + override fun getName(): String = TAG + + @ReactMethod + fun sendRequest(args: ReadableMap, callback: Callback) { + try { + // Prepare request + val request = prepareRestRequest(args) + val returnBinary = args.hasKey(RETURN_BINARY) && args.getBoolean(RETURN_BINARY) + val doesNotRequireAuth = args.hasKey(DOES_NOT_REQUIRE_AUTHENTICATION) + && args.getBoolean(DOES_NOT_REQUIRE_AUTHENTICATION) + + // Sending request + val restClient = getRestClient(doesNotRequireAuth) ?: return // we are detached - do nothing + restClient.sendAsync(request, object : RestClient.AsyncRequestCallback { + + override fun onSuccess(request: RestRequest, response: RestResponse) { + try { + // Not a 2xx status + if (!response.isSuccess) { + val responseObject = JSONObject() + responseObject.put("headers", JSONObject(response.allHeaders)) + responseObject.put("statusCode", response.statusCode) + responseObject.put("body", parsedResponse(response)) + val errorObject = JSONObject() + errorObject.put("response", responseObject) + ReactBridgeHelper.invokeError(callback, errorObject.toString()) + } + // Binary response + else if (returnBinary) { + val result = JSONObject() + result.put(CONTENT_TYPE, response.contentType) + result.put(ENCODED_BODY, Base64.encodeToString(response.asBytes(), Base64.DEFAULT)) + ReactBridgeHelper.invokeSuccess(callback, result) + } + // Other cases + else { + callback.invoke(null, response.asString()) + } + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "sendRequest failed", e) + onError(e) + } + } + + override fun onError(exception: Exception) { + val errorObject = JSONObject() + try { + errorObject.put("error", exception.message) + } catch (jsonException: JSONException) { + SalesforceReactLogger.e(TAG, "Error creating error object", jsonException) + } + ReactBridgeHelper.invokeError(callback, errorObject.toString()) + } + }) + } catch (exception: Exception) { + val errorObject = JSONObject() + try { + errorObject.put("error", exception.message) + } catch (jsonException: JSONException) { + SalesforceReactLogger.e(TAG, "Error creating error object", jsonException) + } + ReactBridgeHelper.invokeError(callback, errorObject.toString()) + } + } + + @Throws(IOException::class) + private fun parsedResponse(response: RestResponse): Any { + // Is it a JSONObject? + val responseAsJSONObject = parseResponseAsJSONObject(response) + if (responseAsJSONObject != null) { + return responseAsJSONObject + } + + // Is it a JSONArray? + val responseAsJSONArray = parseResponseAsJSONArray(response) + if (responseAsJSONArray != null) { + return responseAsJSONArray + } + + // Otherwise return as string + return response.asString() + } + + @Throws(IOException::class) + private fun parseResponseAsJSONObject(response: RestResponse): JSONObject? { + return try { + response.asJSONObject() + } catch (e: JSONException) { + null + } + } + + @Throws(IOException::class) + private fun parseResponseAsJSONArray(response: RestResponse): JSONArray? { + return try { + response.asJSONArray() + } catch (e: JSONException) { + null + } + } + + private fun prepareRestRequest(args: ReadableMap): RestRequest { + // Parse args + val method = RestRequest.RestMethod.valueOf(args.getString(METHOD_KEY)!!) + val endPoint = if (!args.hasKey(END_POINT_KEY) || args.isNull(END_POINT_KEY)) "" else args.getString(END_POINT_KEY)!! + val path = args.getString(PATH_KEY)!! + val queryParams = args.getMap(QUERY_PARAMS_KEY)!! + val headerParams = args.getMap(HEADER_PARAMS_KEY)!! + val fileParams = args.getMap(FILE_PARAMS_KEY)!! + + // Preparing request + val additionalHeaders = ReactBridgeHelper.toJavaStringStringMap(headerParams) + val queryParamsMap = ReactBridgeHelper.toJavaMap(queryParams) + val fileParamsMap = ReactBridgeHelper.toJavaStringMapMap(fileParams) + var urlParams = "" + var requestBody: RequestBody? = null + if (method == RestRequest.RestMethod.DELETE || method == RestRequest.RestMethod.GET + || method == RestRequest.RestMethod.HEAD) { + urlParams = buildQueryString(queryParamsMap) + } else { + requestBody = buildRequestBody(queryParamsMap, fileParamsMap) + } + val separator = if (urlParams.isEmpty()) { + "" + } else if (path.contains("?")) { + if (path.endsWith("&")) "" else "&" + } else { + "?" + } + return RestRequest(method, endPoint + path + separator + urlParams, requestBody, additionalHeaders) + } + + /** + * Returns the RestClient instance being used by this bridge. + * + * @param doesNotRequireAuth True - if an unauthenticated client should be used, False - otherwise. + * @return RestClient instance. + */ + protected open fun getRestClient(doesNotRequireAuth: Boolean): RestClient? { + val currentActivity = getCurrentActivity() as? SalesforceReactActivity ?: return null + return if (doesNotRequireAuth) { + currentActivity.buildClientManager().peekUnauthenticatedRestClient() + } else { + currentActivity.restClient + } + } + + private fun buildQueryString(params: Map): String { + val sb = StringBuilder() + for ((key, value) in params) { + sb.append(key) + .append("=") + .append(URLEncoder.encode(value.toString(), RestRequest.UTF_8)) + .append("&") + } + return sb.toString() + } + + private fun buildRequestBody(params: Map, fileParams: Map>): RequestBody { + val paramsRequestBody = JSONObject(params).toString() + .toRequestBody(RestRequest.MEDIA_TYPE_JSON) + if (fileParams.isEmpty()) { + return paramsRequestBody + } else { + val builder = MultipartBody.Builder().setType(MultipartBody.FORM) + builder.addFormDataPart("", null, paramsRequestBody) + + // File params expected to be of the form: + // {: {fileMimeType:, fileUrl:, fileName:}} + for ((fileParamName, fileParam) in fileParams) { + val mimeType = fileParam[FILE_MIME_TYPE_KEY]!! + val name = fileParam[FILE_NAME_KEY]!! + val url = URI(fileParam[FILE_URL_KEY]!!) + val file = File(url) + val mediaType = mimeType.toMediaType() + builder.addFormDataPart(fileParamName, name, file.asRequestBody(mediaType)) + } + return builder.build() + } + } +} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.java deleted file mode 100644 index 2010a3744e..0000000000 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2015-present, salesforce.com, inc. - * All rights reserved. - * Redistribution and use of this software in source and binary forms, with or - * without modification, are permitted provided that the following conditions - * are met: - * - Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of salesforce.com, inc. nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission of salesforce.com, inc. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package com.salesforce.androidsdk.reactnative.bridge; - -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; -import com.salesforce.androidsdk.reactnative.ui.SalesforceReactActivity; - -public class SalesforceOauthReactBridge extends ReactContextBaseJavaModule { - - private static final String TAG = "SalesforceOauthReactBridge"; - - public SalesforceOauthReactBridge(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public String getName() { - return TAG; - } - - @ReactMethod - public void authenticate(ReadableMap args, - Callback successCallback, Callback errorCallback) { - final SalesforceReactActivity currentActivity = (SalesforceReactActivity) getCurrentActivity(); - if (currentActivity != null) { - currentActivity.authenticate(successCallback, errorCallback); - } - else { - if (errorCallback != null) { - errorCallback.invoke("SalesforceReactActivity not found"); - } - } - } - - - @ReactMethod - public void getAuthCredentials(ReadableMap args, - Callback successCallback, Callback errorCallback) { - final SalesforceReactActivity currentActivity = (SalesforceReactActivity) getCurrentActivity(); - if (currentActivity != null) { - currentActivity.getAuthCredentials(successCallback, errorCallback); - } - else { - if (errorCallback != null) { - errorCallback.invoke("SalesforceReactActivity not found"); - } - } - } - - @ReactMethod - public void logoutCurrentUser(ReadableMap args, - Callback successCallback, Callback errorCallback) { - final SalesforceReactActivity currentActivity = (SalesforceReactActivity) getCurrentActivity(); - if (currentActivity != null) { - currentActivity.logout(successCallback); - } - else { - if (errorCallback != null) { - errorCallback.invoke("SalesforceReactActivity not found"); - } - } - } -} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.kt b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.kt new file mode 100644 index 0000000000..afd536bba6 --- /dev/null +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SalesforceOauthReactBridge.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.reactnative.bridge + +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.turbomodule.core.interfaces.TurboModule +import com.salesforce.androidsdk.reactnative.ui.SalesforceReactActivity + +class SalesforceOauthReactBridge(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { + + override fun getName(): String = TAG + + @ReactMethod + fun authenticate(args: ReadableMap, callback: Callback) { + val currentActivity = getCurrentActivity() as? SalesforceReactActivity + if (currentActivity != null) { + currentActivity.authenticate(callback) + } else { + ReactBridgeHelper.invokeError(callback, "SalesforceReactActivity not found") + } + } + + @ReactMethod + fun getAuthCredentials(args: ReadableMap, callback: Callback) { + val currentActivity = getCurrentActivity() as? SalesforceReactActivity + if (currentActivity != null) { + currentActivity.getAuthCredentials(callback) + } else { + ReactBridgeHelper.invokeError(callback, "SalesforceReactActivity not found") + } + } + + @ReactMethod + fun logoutCurrentUser(args: ReadableMap, callback: Callback) { + val currentActivity = getCurrentActivity() as? SalesforceReactActivity + if (currentActivity != null) { + currentActivity.logout(callback) + } else { + ReactBridgeHelper.invokeError(callback, "SalesforceReactActivity not found") + } + } + + companion object { + private const val TAG = "SalesforceOauthReactBridge" + } +} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.java deleted file mode 100644 index eefd42bebe..0000000000 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.java +++ /dev/null @@ -1,713 +0,0 @@ -/* - * Copyright (c) 2015-present, salesforce.com, inc. - * All rights reserved. - * Redistribution and use of this software in source and binary forms, with or - * without modification, are permitted provided that the following conditions - * are met: - * - Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of salesforce.com, inc. nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission of salesforce.com, inc. - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package com.salesforce.androidsdk.reactnative.bridge; - -import android.util.SparseArray; - -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.salesforce.androidsdk.accounts.UserAccount; -import com.salesforce.androidsdk.accounts.UserAccountManager; -import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger; -import com.salesforce.androidsdk.smartstore.app.SmartStoreSDKManager; -import com.salesforce.androidsdk.smartstore.store.DBOpenHelper; -import com.salesforce.androidsdk.smartstore.store.IndexSpec; -import com.salesforce.androidsdk.smartstore.store.QuerySpec; -import com.salesforce.androidsdk.smartstore.store.SmartStore; -import com.salesforce.androidsdk.smartstore.store.StoreCursor; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class SmartStoreReactBridge extends ReactContextBaseJavaModule { - - // Log tag - static final String TAG = "SmartStoreReactBridge"; - - // Keys in json from/to javascript - static final String RE_INDEX_DATA = "reIndexData"; - static final String CURSOR_ID = "cursorId"; - static final String TYPE = "type"; - static final String SOUP_NAME = "soupName"; - static final String PATH = "path"; - static final String PATHS = "paths"; - static final String QUERY_SPEC = "querySpec"; - static final String SOUP_SPEC = "soupSpec"; - static final String SOUP_SPEC_NAME = "name"; - static final String SOUP_SPEC_FEATURES = "features"; - static final String EXTERNAL_ID_PATH = "externalIdPath"; - static final String ENTRIES = "entries"; - static final String ENTRY_IDS = "entryIds"; - static final String INDEX = "index"; - static final String INDEXES = "indexes"; - static final String IS_GLOBAL_STORE = "isGlobalStore"; - static final String STORE_NAME = "storeName"; - - // Map of cursor id to StoreCursor, per database. - private static Map> STORE_CURSORS = new HashMap>(); - - private synchronized static SparseArray getSmartStoreCursors(SmartStore store) { - final SQLiteDatabase db = store.getDatabase(); - if (!STORE_CURSORS.containsKey(db)) { - STORE_CURSORS.put(db, new SparseArray()); - } - return STORE_CURSORS.get(db); - } - - public SmartStoreReactBridge(ReactApplicationContext reactContext) { - super(reactContext); - } - - @Override - public String getName() { - return "SmartStoreReactBridge"; - } - - /** - * Native implementation of removeFromSoup - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void removeFromSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback){ - - // Parse args - String soupName = args.getString(SOUP_NAME); - - // Run remove - try { - final SmartStore smartStore = getSmartStore(args); - ReadableArray arraySoupEntryIds = (!args.hasKey(ENTRY_IDS) || args.isNull(ENTRY_IDS) ? null : args.getArray(ENTRY_IDS)); - ReadableMap mapQuerySpec = (!args.hasKey(QUERY_SPEC) || args.isNull(QUERY_SPEC) ? null : args.getMap(QUERY_SPEC)); - if (arraySoupEntryIds != null) { - List ids = ReactBridgeHelper.toJavaList(arraySoupEntryIds); - Long[] soupEntryIds = new Long[ids.size()]; - for (int i = 0; i < ids.size(); i++) { - soupEntryIds[i] = ((Double) ids.get(i)).longValue(); - } - smartStore.delete(soupName, soupEntryIds); - } else { - JSONObject querySpecJson = new JSONObject(ReactBridgeHelper.toJavaMap(mapQuerySpec)); - QuerySpec querySpec = QuerySpec.fromJSON(soupName, querySpecJson); - smartStore.deleteByQuery(soupName, querySpec); - } - successCallback.invoke(); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "removeFromSoup call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of retrieveSoupEntries - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void retrieveSoupEntries(ReadableMap args, final Callback successCallback, - final Callback errorCallback){ - - // Parse args - String soupName = args.getString(SOUP_NAME); - - // Run retrieve - try { - final SmartStore smartStore = getSmartStore(args); - Double[] soupEntryIdsFromJs = ReactBridgeHelper.toJavaList(args.getArray(ENTRY_IDS)).toArray(new Double[0]); // we get Double's back - Long[] soupEntryIds = new Long[soupEntryIdsFromJs.length]; - for (int i=0; i entries = new ArrayList(); - for (int i = 0; i < entriesList.size(); i++) { - entries.add(new JSONObject((Map) entriesList.get(i))); - } - - // Run upsert - synchronized(smartStore.getDatabase()) { - smartStore.beginTransaction(); - try { - JSONArray results = new JSONArray(); - for (JSONObject entry : entries) { - results.put(smartStore.upsert(soupName, entry, externalIdPath, false)); - } - smartStore.setTransactionSuccessful(); - ReactBridgeHelper.invoke(successCallback, results); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "upsertSoupEntries call failed", e); - errorCallback.invoke(e.toString()); - } finally { - smartStore.endTransaction(); - } - } - } - - /** - * Native implementation of registerSoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void registerSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - try { - // Parse args. - final SmartStore smartStore = getSmartStore(args); - String soupName = args.isNull(SOUP_NAME) ? null : args.getString(SOUP_NAME); - IndexSpec[] indexSpecs = getIndexSpecsFromArg(args); - - smartStore.registerSoup(soupName, indexSpecs); - ReactBridgeHelper.invoke(successCallback, soupName); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "registerSoup call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of querySoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void querySoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback){ - - // Parse args - String soupName = args.getString(SOUP_NAME); - try { - final SmartStore smartStore = getSmartStore(args); - JSONObject querySpecJson = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(QUERY_SPEC))); - QuerySpec querySpec = QuerySpec.fromJSON(soupName, querySpecJson); - if (querySpec.queryType == QuerySpec.QueryType.smart) { - throw new RuntimeException("Smart queries can only be run through runSmartQuery"); - } - - // Run query - runQuery(smartStore, querySpec, successCallback); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "querySoup call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of runSmartSql - * @param args - * @param successCallback - * @param errorCallback - */ - @ReactMethod - public void runSmartQuery(ReadableMap args, final Callback successCallback, - final Callback errorCallback){ - - // Parse args - JSONObject querySpecJson = new JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(QUERY_SPEC))); - try { - final SmartStore smartStore = getSmartStore(args); - QuerySpec querySpec = QuerySpec.fromJSON(null, querySpecJson); - if (querySpec.queryType != QuerySpec.QueryType.smart) { - throw new RuntimeException("runSmartQuery can only run smart queries"); - } - - // Run query - runQuery(smartStore, querySpec, successCallback); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "runSmartQuery call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Helper for querySoup and runSmartSql - * @param querySpec - * @param successCallback - * @throws JSONException - */ - private void runQuery(SmartStore smartStore, QuerySpec querySpec, - final Callback successCallback) throws JSONException { - - // Build store cursor - final StoreCursor storeCursor = new StoreCursor(smartStore, querySpec); - getSmartStoreCursors(smartStore).put(storeCursor.cursorId, storeCursor); - - // Build json result - JSONObject result = storeCursor.getDataSerialized(smartStore); - - // Done - ReactBridgeHelper.invoke(successCallback, result); - } - - /** - * Native implementation of removeSoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void removeSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - try { - - // Parse args - String soupName = args.getString(SOUP_NAME); - final SmartStore smartStore = getSmartStore(args); - - // Run remove - smartStore.dropSoup(soupName); - successCallback.invoke(); - } catch (Exception e) { - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of clearSoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void clearSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - try { - - // Parse args - String soupName = args.getString(SOUP_NAME); - final SmartStore smartStore = getSmartStore(args); - - // Run clear - smartStore.clearSoup(soupName); - successCallback.invoke(); - } catch (Exception e) { - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of getDatabaseSize - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void getDatabaseSize(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - try { - - // Parse args - final SmartStore smartStore = getSmartStore(args); - int databaseSize = smartStore.getDatabaseSize(); - ReactBridgeHelper.invoke(successCallback, databaseSize); - } catch (Exception e) { - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of alterSoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void alterSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - try { - - // Parse args. - final SmartStore smartStore = getSmartStore(args); - String soupName = args.getString(SOUP_NAME); - IndexSpec[] indexSpecs = getIndexSpecsFromArg(args); - boolean reIndexData = args.getBoolean(RE_INDEX_DATA); - - smartStore.alterSoup(soupName, indexSpecs, reIndexData); - ReactBridgeHelper.invoke(successCallback, soupName); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "alterSoup call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of reIndexSoup - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void reIndexSoup(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - - // Parse args - String soupName = args.getString(SOUP_NAME); - try { - final SmartStore smartStore = getSmartStore(args); - List indexPaths = ReactBridgeHelper.toJavaStringList(args.getArray(PATHS)); - - // Run register - smartStore.reIndexSoup(soupName, indexPaths.toArray(new String[0]), true); - ReactBridgeHelper.invoke(successCallback, soupName); - } catch (Exception e) { - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of getSoupIndexSpecs - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void getSoupIndexSpecs(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - - // Get soup index specs - try { - - // Parse args - String soupName = args.getString(SOUP_NAME); - final SmartStore smartStore = getSmartStore(args); - IndexSpec[] indexSpecs = smartStore.getSoupIndexSpecs(soupName); - JSONArray indexSpecsJson = new JSONArray(); - for (int i = 0; i < indexSpecs.length; i++) { - JSONObject indexSpecJson = new JSONObject(); - IndexSpec indexSpec = indexSpecs[i]; - indexSpecJson.put(PATH, indexSpec.path); - indexSpecJson.put(TYPE, indexSpec.type); - indexSpecsJson.put(indexSpecJson); - } - ReactBridgeHelper.invoke(successCallback, indexSpecsJson); - } catch (Exception e) { - SalesforceReactLogger.e(TAG, "getSoupIndexSpecs call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of getAllGlobalStores - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void getAllGlobalStores(ReadableMap args, final Callback successCallback, - final Callback errorCallback) throws JSONException { - // return list of StoreConfigs - List globalDBNames = SmartStoreSDKManager.getInstance().getGlobalStoresPrefixList(); - JSONArray storeList = new JSONArray(); - try { - if(globalDBNames !=null ) { - for (int i = 0; i < globalDBNames.size(); i++) { - JSONObject dbName = new JSONObject(); - dbName.put(IS_GLOBAL_STORE,true); - dbName.put(STORE_NAME,globalDBNames.get(i)); - storeList.put(dbName); - } - } - ReactBridgeHelper.invoke(successCallback, storeList); - } catch (JSONException e) { - SalesforceReactLogger.e(TAG, "getAllGlobalStorePrefixes call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of getAllStores - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void getAllStores(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - // return list of StoreConfigs - List userStoreNames = SmartStoreSDKManager.getInstance().getUserStoresPrefixList(); - JSONArray storeList = new JSONArray(); - try { - if(userStoreNames !=null ) { - for (int i = 0; i < userStoreNames.size(); i++) { - JSONObject dbName = new JSONObject(); - dbName.put(IS_GLOBAL_STORE,false); - dbName.put(STORE_NAME,userStoreNames.get(i)); - storeList.put(dbName); - } - } - ReactBridgeHelper.invoke(successCallback, storeList); - } catch (JSONException e) { - SalesforceReactLogger.e(TAG, "getAllStorePrefixes call failed", e); - errorCallback.invoke(e.toString()); - } - } - - /** - * Native implementation of removeStore - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void removeStore(ReadableMap args, final Callback successCallback, - final Callback errorCallback){ - - boolean isGlobal = SmartStoreReactBridge.getIsGlobal(args); - final String storeName = SmartStoreReactBridge.getStoreName(args); - if (isGlobal) { - SmartStoreSDKManager.getInstance().removeGlobalSmartStore(storeName); - ReactBridgeHelper.invoke(successCallback, true); - } else { - final UserAccount account = UserAccountManager.getInstance().getCachedCurrentUser(); - if (account == null) { - errorCallback.invoke("No user account found"); - } else { - SmartStoreSDKManager.getInstance().removeSmartStore(storeName, account, account.getCommunityId()); - ReactBridgeHelper.invoke(successCallback, true); - } - } - } - - /** - * Native implementation of removeAllGlobalStores - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void removeAllGlobalStores(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - SmartStoreSDKManager.getInstance().removeAllGlobalStores(); - ReactBridgeHelper.invoke(successCallback, true); - } - - /** - * Native implementation of removeAllStores - * @param args - * @param successCallback - * @param errorCallback - * @return - */ - @ReactMethod - public void removeAllStores(ReadableMap args, final Callback successCallback, - final Callback errorCallback) { - SmartStoreSDKManager.getInstance().removeAllUserStores(); - ReactBridgeHelper.invoke(successCallback, true); - } - - /** - * Return the value of the isGlobalStore argument - * @param args - * @return - */ - private static boolean getIsGlobal(ReadableMap args) { - return args != null ? args.getBoolean(IS_GLOBAL_STORE) : false; - } - - /** - * Return smartstore to use - * @param args arguments passed in bridge call - * @return - */ - public static SmartStore getSmartStore(ReadableMap args) throws Exception { - boolean isGlobal = getIsGlobal(args); - final String storeName = getStoreName(args); - if (isGlobal) { - return SmartStoreSDKManager.getInstance().getGlobalSmartStore(storeName); - } else { - final UserAccount account = UserAccountManager.getInstance().getCachedCurrentUser(); - if (account == null) { - throw new Exception("No user account found"); - } else { - return SmartStoreSDKManager.getInstance().getSmartStore(storeName, account, account.getCommunityId()); - } - } - } - - /** - * Return the value of the storename argument - * @param args arguments passed in bridge call - * @return - */ - private static String getStoreName(ReadableMap args) { - String storeName = args != null && args.hasKey(STORE_NAME) ? args.getString(STORE_NAME) : DBOpenHelper.DEFAULT_DB_NAME; - return (storeName!=null && storeName.trim().length()>0) ? storeName : DBOpenHelper.DEFAULT_DB_NAME; - } - - /** - * Build index specs array from javascript argument - * @param args - * @return - * @throws JSONException - */ - private IndexSpec[] getIndexSpecsFromArg(ReadableMap args) throws JSONException { - JSONArray indexesJson = new JSONArray(ReactBridgeHelper.toJavaList(args.getArray(INDEXES))); - return IndexSpec.fromJSON(indexesJson); - } -} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.kt b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.kt new file mode 100644 index 0000000000..ea2e4b051c --- /dev/null +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/bridge/SmartStoreReactBridge.kt @@ -0,0 +1,526 @@ +/* + * Copyright (c) 2015-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.reactnative.bridge + +import android.util.SparseArray +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.turbomodule.core.interfaces.TurboModule +import com.salesforce.androidsdk.accounts.UserAccountManager +import com.salesforce.androidsdk.reactnative.util.SalesforceReactLogger +import com.salesforce.androidsdk.smartstore.app.SmartStoreSDKManager +import com.salesforce.androidsdk.smartstore.store.DBOpenHelper +import com.salesforce.androidsdk.smartstore.store.IndexSpec +import com.salesforce.androidsdk.smartstore.store.QuerySpec +import com.salesforce.androidsdk.smartstore.store.SmartStore +import com.salesforce.androidsdk.smartstore.store.StoreCursor +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class SmartStoreReactBridge(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { + + override fun getName(): String = "SmartStoreReactBridge" + + /** + * Native implementation of removeFromSoup + */ + @ReactMethod + fun removeFromSoup(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val arraySoupEntryIds = if (!args.hasKey(ENTRY_IDS) || args.isNull(ENTRY_IDS)) null else args.getArray(ENTRY_IDS) + val mapQuerySpec = if (!args.hasKey(QUERY_SPEC) || args.isNull(QUERY_SPEC)) null else args.getMap(QUERY_SPEC) + if (arraySoupEntryIds != null) { + val ids = ReactBridgeHelper.toJavaList(arraySoupEntryIds) + val soupEntryIds = Array(ids.size) { i -> (ids[i] as Double).toLong() } + smartStore.delete(soupName, *soupEntryIds) + } else { + val querySpecJson = JSONObject(ReactBridgeHelper.toJavaMap(mapQuerySpec)) + val querySpec = QuerySpec.fromJSON(soupName, querySpecJson) + smartStore.deleteByQuery(soupName, querySpec) + } + ReactBridgeHelper.invokeSuccess(callback) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "removeFromSoup call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of retrieveSoupEntries + */ + @ReactMethod + fun retrieveSoupEntries(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val soupEntryIdsFromJs = ReactBridgeHelper.toJavaList(args.getArray(ENTRY_IDS)).toTypedArray() + val soupEntryIds = Array(soupEntryIdsFromJs.size) { i -> (soupEntryIdsFromJs[i] as Double).toLong() } + val result = smartStore.retrieve(soupName, *soupEntryIds) + ReactBridgeHelper.invokeSuccess(callback, result) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "retrieveSoupEntries call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of closeCursor + */ + @ReactMethod + fun closeCursor(args: ReadableMap, callback: Callback) { + try { + val cursorId = args.getInt(CURSOR_ID) + val smartStore = getSmartStore(args) + getSmartStoreCursors(smartStore).remove(cursorId) + ReactBridgeHelper.invokeSuccess(callback) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of moveCursorToPageIndex + */ + @ReactMethod + fun moveCursorToPageIndex(args: ReadableMap, callback: Callback) { + val cursorId = args.getInt(CURSOR_ID) + val index = args.getInt(INDEX) + val smartStore: SmartStore + try { + smartStore = getSmartStore(args) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + return + } + + // Get cursor + val storeCursor = getSmartStoreCursors(smartStore).get(cursorId) + if (storeCursor == null) { + ReactBridgeHelper.invokeError(callback, "Invalid cursor id") + return + } + + // Change page + storeCursor.moveToPageIndex(index) + + // Build json result + val result = storeCursor.getDataSerialized(smartStore) + ReactBridgeHelper.invokeSuccess(callback, result) + } + + /** + * Native implementation of soupExists + */ + @ReactMethod + fun soupExists(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val exists = smartStore.hasSoup(soupName) + ReactBridgeHelper.invokeSuccess(callback, exists) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of upsertSoupEntries + */ + @ReactMethod + fun upsertSoupEntries(args: ReadableMap, callback: Callback) { + val soupName = args.getString(SOUP_NAME) + val smartStore: SmartStore + try { + smartStore = getSmartStore(args) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + return + } + val entriesList = ReactBridgeHelper.toJavaList(args.getArray(ENTRIES)) + val externalIdPath = args.getString(EXTERNAL_ID_PATH) + val entries = entriesList.map { JSONObject(it as Map<*, *>) } + + // Run upsert + synchronized(smartStore.database) { + smartStore.beginTransaction() + try { + val results = JSONArray() + for (entry in entries) { + results.put(smartStore.upsert(soupName, entry, externalIdPath, false)) + } + smartStore.setTransactionSuccessful() + ReactBridgeHelper.invokeSuccess(callback, results) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "upsertSoupEntries call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } finally { + smartStore.endTransaction() + } + } + } + + /** + * Native implementation of registerSoup + */ + @ReactMethod + fun registerSoup(args: ReadableMap, callback: Callback) { + try { + val smartStore = getSmartStore(args) + val soupName = if (args.isNull(SOUP_NAME)) null else args.getString(SOUP_NAME) + val indexSpecs = getIndexSpecsFromArg(args) + smartStore.registerSoup(soupName, indexSpecs) + ReactBridgeHelper.invokeSuccess(callback, soupName!!) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "registerSoup call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of querySoup + */ + @ReactMethod + fun querySoup(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val querySpecJson = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(QUERY_SPEC))) + val querySpec = QuerySpec.fromJSON(soupName, querySpecJson) + if (querySpec.queryType == QuerySpec.QueryType.smart) { + throw RuntimeException("Smart queries can only be run through runSmartQuery") + } + runQuery(smartStore, querySpec, callback) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "querySoup call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of runSmartSql + */ + @ReactMethod + fun runSmartQuery(args: ReadableMap, callback: Callback) { + val querySpecJson = JSONObject(ReactBridgeHelper.toJavaMap(args.getMap(QUERY_SPEC))) + try { + val smartStore = getSmartStore(args) + val querySpec = QuerySpec.fromJSON(null, querySpecJson) + if (querySpec.queryType != QuerySpec.QueryType.smart) { + throw RuntimeException("runSmartQuery can only run smart queries") + } + runQuery(smartStore, querySpec, callback) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "runSmartQuery call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Helper for querySoup and runSmartSql + */ + @Throws(JSONException::class) + private fun runQuery(smartStore: SmartStore, querySpec: QuerySpec, callback: Callback) { + val storeCursor = StoreCursor(smartStore, querySpec) + getSmartStoreCursors(smartStore).put(storeCursor.cursorId, storeCursor) + val result = storeCursor.getDataSerialized(smartStore) + ReactBridgeHelper.invokeSuccess(callback, result) + } + + /** + * Native implementation of removeSoup + */ + @ReactMethod + fun removeSoup(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + smartStore.dropSoup(soupName) + ReactBridgeHelper.invokeSuccess(callback) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of clearSoup + */ + @ReactMethod + fun clearSoup(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + smartStore.clearSoup(soupName) + ReactBridgeHelper.invokeSuccess(callback) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of getDatabaseSize + */ + @ReactMethod + fun getDatabaseSize(args: ReadableMap, callback: Callback) { + try { + val smartStore = getSmartStore(args) + val databaseSize = smartStore.databaseSize + ReactBridgeHelper.invokeSuccess(callback, databaseSize) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of alterSoup + */ + @ReactMethod + fun alterSoup(args: ReadableMap, callback: Callback) { + try { + val smartStore = getSmartStore(args) + val soupName = args.getString(SOUP_NAME) + val indexSpecs = getIndexSpecsFromArg(args) + val reIndexData = args.getBoolean(RE_INDEX_DATA) + smartStore.alterSoup(soupName, indexSpecs, reIndexData) + ReactBridgeHelper.invokeSuccess(callback, soupName!!) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "alterSoup call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of reIndexSoup + */ + @ReactMethod + fun reIndexSoup(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val indexPaths = ReactBridgeHelper.toJavaStringList(args.getArray(PATHS)) + smartStore.reIndexSoup(soupName, indexPaths.toTypedArray(), true) + ReactBridgeHelper.invokeSuccess(callback, soupName!!) + } catch (e: Exception) { + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of getSoupIndexSpecs + */ + @ReactMethod + fun getSoupIndexSpecs(args: ReadableMap, callback: Callback) { + try { + val soupName = args.getString(SOUP_NAME) + val smartStore = getSmartStore(args) + val indexSpecs = smartStore.getSoupIndexSpecs(soupName) + val indexSpecsJson = JSONArray() + for (indexSpec in indexSpecs) { + val indexSpecJson = JSONObject() + indexSpecJson.put(PATH, indexSpec.path) + indexSpecJson.put(TYPE, indexSpec.type) + indexSpecsJson.put(indexSpecJson) + } + ReactBridgeHelper.invokeSuccess(callback, indexSpecsJson) + } catch (e: Exception) { + SalesforceReactLogger.e(TAG, "getSoupIndexSpecs call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of getAllGlobalStores + */ + @ReactMethod + fun getAllGlobalStores(args: ReadableMap, callback: Callback) { + try { + val globalDBNames = SmartStoreSDKManager.getInstance().globalStoresPrefixList + val storeList = JSONArray() + if (globalDBNames != null) { + for (name in globalDBNames) { + val dbName = JSONObject() + dbName.put(IS_GLOBAL_STORE, true) + dbName.put(STORE_NAME, name) + storeList.put(dbName) + } + } + ReactBridgeHelper.invokeSuccess(callback, storeList) + } catch (e: JSONException) { + SalesforceReactLogger.e(TAG, "getAllGlobalStorePrefixes call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of getAllStores + */ + @ReactMethod + fun getAllStores(args: ReadableMap, callback: Callback) { + try { + val userStoreNames = SmartStoreSDKManager.getInstance().userStoresPrefixList + val storeList = JSONArray() + if (userStoreNames != null) { + for (name in userStoreNames) { + val dbName = JSONObject() + dbName.put(IS_GLOBAL_STORE, false) + dbName.put(STORE_NAME, name) + storeList.put(dbName) + } + } + ReactBridgeHelper.invokeSuccess(callback, storeList) + } catch (e: JSONException) { + SalesforceReactLogger.e(TAG, "getAllStorePrefixes call failed", e) + ReactBridgeHelper.invokeError(callback, e.toString()) + } + } + + /** + * Native implementation of removeStore + */ + @ReactMethod + fun removeStore(args: ReadableMap, callback: Callback) { + val isGlobal = getIsGlobal(args) + val storeName = getStoreName(args) + if (isGlobal) { + SmartStoreSDKManager.getInstance().removeGlobalSmartStore(storeName) + ReactBridgeHelper.invokeSuccess(callback, true) + } else { + val account = UserAccountManager.getInstance().cachedCurrentUser + if (account == null) { + ReactBridgeHelper.invokeError(callback, "No user account found") + } else { + SmartStoreSDKManager.getInstance().removeSmartStore(storeName, account, account.communityId) + ReactBridgeHelper.invokeSuccess(callback, true) + } + } + } + + /** + * Native implementation of removeAllGlobalStores + */ + @ReactMethod + fun removeAllGlobalStores(args: ReadableMap, callback: Callback) { + SmartStoreSDKManager.getInstance().removeAllGlobalStores() + ReactBridgeHelper.invokeSuccess(callback, true) + } + + /** + * Native implementation of removeAllStores + */ + @ReactMethod + fun removeAllStores(args: ReadableMap, callback: Callback) { + SmartStoreSDKManager.getInstance().removeAllUserStores() + ReactBridgeHelper.invokeSuccess(callback, true) + } + + /** + * Build index specs array from javascript argument + */ + @Throws(JSONException::class) + private fun getIndexSpecsFromArg(args: ReadableMap): Array { + val indexesJson = JSONArray(ReactBridgeHelper.toJavaList(args.getArray(INDEXES))) + return IndexSpec.fromJSON(indexesJson) + } + + companion object { + // Log tag + const val TAG = "SmartStoreReactBridge" + + // Keys in json from/to javascript + const val RE_INDEX_DATA = "reIndexData" + const val CURSOR_ID = "cursorId" + const val TYPE = "type" + const val SOUP_NAME = "soupName" + const val PATH = "path" + const val PATHS = "paths" + const val QUERY_SPEC = "querySpec" + const val SOUP_SPEC = "soupSpec" + const val SOUP_SPEC_NAME = "name" + const val SOUP_SPEC_FEATURES = "features" + const val EXTERNAL_ID_PATH = "externalIdPath" + const val ENTRIES = "entries" + const val ENTRY_IDS = "entryIds" + const val INDEX = "index" + const val INDEXES = "indexes" + const val IS_GLOBAL_STORE = "isGlobalStore" + const val STORE_NAME = "storeName" + + // Map of cursor id to StoreCursor, per database. + private val STORE_CURSORS = HashMap>() + + @Synchronized + @JvmStatic + fun getSmartStoreCursors(store: SmartStore): SparseArray { + val db = store.database + if (!STORE_CURSORS.containsKey(db)) { + STORE_CURSORS[db] = SparseArray() + } + return STORE_CURSORS[db]!! + } + + /** + * Return the value of the isGlobalStore argument + */ + @JvmStatic + fun getIsGlobal(args: ReadableMap?): Boolean { + return args?.getBoolean(IS_GLOBAL_STORE) ?: false + } + + /** + * Return the value of the storename argument + */ + @JvmStatic + fun getStoreName(args: ReadableMap?): String { + val storeName = if (args != null && args.hasKey(STORE_NAME)) args.getString(STORE_NAME) else DBOpenHelper.DEFAULT_DB_NAME + return if (!storeName.isNullOrBlank()) storeName else DBOpenHelper.DEFAULT_DB_NAME + } + + /** + * Return smartstore to use + */ + @JvmStatic + @Throws(Exception::class) + fun getSmartStore(args: ReadableMap): SmartStore { + val isGlobal = getIsGlobal(args) + val storeName = getStoreName(args) + return if (isGlobal) { + SmartStoreSDKManager.getInstance().getGlobalSmartStore(storeName) + } else { + val account = UserAccountManager.getInstance().cachedCurrentUser + ?: throw Exception("No user account found") + SmartStoreSDKManager.getInstance().getSmartStore(storeName, account, account.communityId) + } + } + } +} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/ui/SalesforceReactActivity.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/ui/SalesforceReactActivity.java index 745f233500..93311c781d 100644 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/ui/SalesforceReactActivity.java +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/ui/SalesforceReactActivity.java @@ -60,21 +60,22 @@ public abstract class SalesforceReactActivity extends ReactActivity implements S AlertDialog overlayPermissionRequiredDialog; /** - * Pending callbacks for authentication requests from the React Native bridge. + * Pending callback for authentication requests from the React Native bridge. * * When authenticate() is called from JavaScript: - * - These callbacks are stored in pending variables - * - They are invoked once authentication completes (either immediately if already + * - This callback is stored in a pending variable + * - It is invoked once authentication completes (either immediately if already * authenticated, or after OAuth flow completes) - * - Two code paths can invoke these: authenticatedRestClient() callback (always runs) + * - Two code paths can invoke it: authenticatedRestClient() callback (always runs) * or onResume() (only runs after OAuth pause/resume cycle) - * - Whichever path runs first invokes the callbacks and clears these to null + * - Whichever path runs first invokes the callback and clears it to null * - The other path sees null and does nothing, preventing double invocation * + * Uses single-callback pattern: callback(null, result) for success, callback(error) for error. + * * See authenticate() and onResume(RestClient) for the coordination logic. */ - private Callback pendingAuthSuccessCallback; - private Callback pendingAuthErrorCallback; + private Callback pendingAuthCallback; protected SalesforceReactActivity() { super(); @@ -131,22 +132,21 @@ public void onResume(RestClient c) { else { SalesforceReactLogger.i(TAG, "onResume - already logged in"); - // If we have pending auth callbacks (from deferred authentication via authenticate()), - // invoke them now. This handles the OAuth flow scenario where the activity was paused + // If we have a pending auth callback (from deferred authentication via authenticate()), + // invoke it now. This handles the OAuth flow scenario where the activity was paused // for login and is now resuming. // // NOTE: This works in coordination with authenticate()'s authenticatedRestClient callback. // In the OAuth flow, there's a race condition between onResume() and authenticatedRestClient(). - // Whichever runs first will find pending callbacks non-null, invoke them, and set them to null. - // The other will find them null and do nothing. This ensures callbacks are invoked exactly once. + // Whichever runs first will find pending callback non-null, invoke it, and set it to null. + // The other will find it null and do nothing. This ensures the callback is invoked exactly once. // - // For the "already authenticated" scenario, authenticatedRestClient() invokes callbacks + // For the "already authenticated" scenario, authenticatedRestClient() invokes the callback // immediately without any pause/resume cycle, so this code is never reached. - if (pendingAuthSuccessCallback != null) { - SalesforceReactLogger.i(TAG, "onResume - invoking pending auth callbacks"); - getAuthCredentials(pendingAuthSuccessCallback, pendingAuthErrorCallback); - pendingAuthSuccessCallback = null; - pendingAuthErrorCallback = null; + if (pendingAuthCallback != null) { + SalesforceReactLogger.i(TAG, "onResume - invoking pending auth callback"); + getAuthCredentials(pendingAuthCallback); + pendingAuthCallback = null; } } } @@ -216,46 +216,41 @@ public void authenticatedRestClient(RestClient client) { /** * Method called from bridge to logout. * - * @param successCallback Success callback. + * @param callback Single callback: callback(null, result) on success. */ - public void logout(final Callback successCallback) { + public void logout(final Callback callback) { SalesforceReactLogger.i(TAG, "logout called"); SalesforceReactSDKManager.getInstance().logout(this); - if (successCallback != null) { - ReactBridgeHelper.invoke(successCallback, "Logout complete"); + if (callback != null) { + ReactBridgeHelper.invokeSuccess(callback, "Logout complete"); } } /** * Method called from bridge to authenticate. * - * @param successCallback Success callback. - * @param errorCallback Error callback. + * @param callback Single callback: callback(null, result) on success, callback(error) on error. */ - public void authenticate(final Callback successCallback, final Callback errorCallback) { + public void authenticate(final Callback callback) { SalesforceReactLogger.i(TAG, "authenticate called"); - // Store callbacks in pending variables to handle both authentication scenarios: + // Store callback in pending variable to handle both authentication scenarios: // // SCENARIO 1: Already authenticated (no OAuth needed) // - getRestClient() callback is invoked immediately on the same thread - // - authenticatedRestClient() below invokes callbacks immediately + // - authenticatedRestClient() below invokes callback immediately // - Activity does NOT pause/resume, so onResume() is NOT called again - // - Callbacks are successfully invoked ✓ + // - Callback is successfully invoked ✓ // // SCENARIO 2: OAuth required (activity will pause/resume) // - getRestClient() starts OAuth flow // - Activity pauses (goes to login screen) // - User completes OAuth, activity resumes // - Either authenticatedRestClient() or onResume() runs first (race condition) - // - Whichever runs first invokes callbacks and clears pending variables - // - The other sees null pending variables and does nothing - // - Callbacks are successfully invoked exactly once ✓ - // - // The key fix: authenticatedRestClient() must ALWAYS invoke and clear callbacks, - // not defer to onResume(), because onResume() is NOT called when already authenticated. - pendingAuthSuccessCallback = successCallback; - pendingAuthErrorCallback = errorCallback; + // - Whichever runs first invokes callback and clears pending variable + // - The other sees null pending variable and does nothing + // - Callback is successfully invoked exactly once ✓ + pendingAuthCallback = callback; clientManager.getRestClient(this, new RestClientCallback() { @@ -264,15 +259,10 @@ public void authenticatedRestClient(RestClient client) { SalesforceReactLogger.i(TAG, "authenticatedRestClient callback invoked"); SalesforceReactActivity.this.setRestClient(client); - // Invoke callbacks immediately now that we have a RestClient. - // For Scenario 1 (already authenticated): This happens immediately, no pause/resume. - // For Scenario 2 (OAuth required): This may happen before or after onResume(). - // In both cases, we invoke callbacks and clear pending variables to prevent double invocation. - if (pendingAuthSuccessCallback != null) { - SalesforceReactLogger.i(TAG, "authenticatedRestClient - invoking pending callbacks"); - getAuthCredentials(pendingAuthSuccessCallback, pendingAuthErrorCallback); - pendingAuthSuccessCallback = null; - pendingAuthErrorCallback = null; + if (pendingAuthCallback != null) { + SalesforceReactLogger.i(TAG, "authenticatedRestClient - invoking pending callback"); + getAuthCredentials(pendingAuthCallback); + pendingAuthCallback = null; } } }); @@ -281,18 +271,17 @@ public void authenticatedRestClient(RestClient client) { /** * Method called from bridge to get auth credentials. * - * @param successCallback Success callback. - * @param errorCallback Error callback. + * @param callback Single callback: callback(null, result) on success, callback(error) on error. */ - public void getAuthCredentials(Callback successCallback, Callback errorCallback) { + public void getAuthCredentials(Callback callback) { SalesforceReactLogger.i(TAG, "getAuthCredentials called"); if (client != null) { - if (successCallback != null) { - ReactBridgeHelper.invoke(successCallback, client.getJSONCredentials()); + if (callback != null) { + ReactBridgeHelper.invokeSuccess(callback, client.getJSONCredentials()); } } else { - if (errorCallback != null) { - errorCallback.invoke("Not authenticated"); + if (callback != null) { + ReactBridgeHelper.invokeError(callback, "Not authenticated"); } } } From 30267c613b2e28ecb9862eb93111138ae4f05195 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 22 May 2026 15:22:47 -0700 Subject: [PATCH 3/6] Point react-native-force at wmathurin#rn-migration for CI test bundle NOTE: This must be reverted before merging to dev. --- libs/SalesforceReact/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/SalesforceReact/package.json b/libs/SalesforceReact/package.json index 683c5ddfe5..a289602583 100644 --- a/libs/SalesforceReact/package.json +++ b/libs/SalesforceReact/package.json @@ -11,7 +11,7 @@ "jsc-android": "^250231.0.0", "react": "19.1.0", "react-native": "0.81.5", - "react-native-force": "git+https://github.com/forcedotcom/SalesforceMobileSDK-ReactNative.git#dev" + "react-native-force": "git+https://github.com/wmathurin/SalesforceMobileSDK-ReactNative.git#rn-migration" }, "devDependencies": { "@babel/core": "^7.25.2", From 350355b952824f5361d2aea32a0d603d823843b1 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 22 May 2026 15:41:25 -0700 Subject: [PATCH 4/6] Fix: Use BaseReactPackage for TurboModule discovery in new arch In bridgeless/new architecture mode, modules registered via plain ReactPackage.createNativeModules() are not discoverable through TurboModuleRegistry. Switch to BaseReactPackage with getModule() and ReactModuleInfoProvider so modules are found by name. --- .../app/SalesforceReactSDKManager.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java index c147d608cb..4c73a10459 100644 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java @@ -34,10 +34,12 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.facebook.react.BaseReactPackage; import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager; import com.salesforce.androidsdk.reactnative.bridge.MobileSyncReactBridge; @@ -49,8 +51,8 @@ import com.salesforce.androidsdk.util.EventsObservable; import com.salesforce.androidsdk.util.EventsObservable.EventType; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -136,24 +138,42 @@ public String getAppType() { * @return ReactPackage for this application */ public ReactPackage getReactPackage() { - return new ReactPackage() { + return new BaseReactPackage() { - @NonNull @Override - public List createNativeModules( + public NativeModule getModule( + @NonNull String name, @NonNull ReactApplicationContext reactContext ) { - List modules = new ArrayList<>(); - modules.add(new SalesforceOauthReactBridge(reactContext)); - modules.add(new SalesforceNetReactBridge(reactContext)); - modules.add(new SmartStoreReactBridge(reactContext)); - modules.add(new MobileSyncReactBridge(reactContext)); - return modules; + switch (name) { + case "SalesforceOauthReactBridge": + return new SalesforceOauthReactBridge(reactContext); + case "SalesforceNetReactBridge": + return new SalesforceNetReactBridge(reactContext); + case "SmartStoreReactBridge": + return new SmartStoreReactBridge(reactContext); + case "MobileSyncReactBridge": + return new MobileSyncReactBridge(reactContext); + default: + return null; + } } - /** @noinspection unused*/ - public List> createJSModules() { - return Collections.emptyList(); + @NonNull + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + Map map = new HashMap<>(); + map.put("SalesforceOauthReactBridge", new ReactModuleInfo( + "SalesforceOauthReactBridge", "SalesforceOauthReactBridge", false, false, false, true)); + map.put("SalesforceNetReactBridge", new ReactModuleInfo( + "SalesforceNetReactBridge", "SalesforceNetReactBridge", false, false, false, true)); + map.put("SmartStoreReactBridge", new ReactModuleInfo( + "SmartStoreReactBridge", "SmartStoreReactBridge", false, false, false, true)); + map.put("MobileSyncReactBridge", new ReactModuleInfo( + "MobileSyncReactBridge", "MobileSyncReactBridge", false, false, false, true)); + return map; + }; } @NonNull From 60f4278ab44b5e05dd26558bc882ac4e7ee0545e Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 22 May 2026 16:05:13 -0700 Subject: [PATCH 5/6] Revert to plain ReactPackage - rely on TurboModule interop BaseReactPackage approach didn't work at runtime. Reverting to plain ReactPackage with createNativeModules(). Since our modules implement TurboModule, the RN interop layer should make them available via TurboModuleRegistry.get() automatically. --- .../app/SalesforceReactSDKManager.java | 45 ++++--------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java index 4c73a10459..425b0f7327 100644 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java @@ -34,12 +34,9 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.facebook.react.BaseReactPackage; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.module.model.ReactModuleInfo; -import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager; import com.salesforce.androidsdk.reactnative.bridge.MobileSyncReactBridge; @@ -52,7 +49,6 @@ import com.salesforce.androidsdk.util.EventsObservable.EventType; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -138,42 +134,19 @@ public String getAppType() { * @return ReactPackage for this application */ public ReactPackage getReactPackage() { - return new BaseReactPackage() { + return new ReactPackage() { + @NonNull @Override - public NativeModule getModule( - @NonNull String name, + public List createNativeModules( @NonNull ReactApplicationContext reactContext ) { - switch (name) { - case "SalesforceOauthReactBridge": - return new SalesforceOauthReactBridge(reactContext); - case "SalesforceNetReactBridge": - return new SalesforceNetReactBridge(reactContext); - case "SmartStoreReactBridge": - return new SmartStoreReactBridge(reactContext); - case "MobileSyncReactBridge": - return new MobileSyncReactBridge(reactContext); - default: - return null; - } - } - - @NonNull - @Override - public ReactModuleInfoProvider getReactModuleInfoProvider() { - return () -> { - Map map = new HashMap<>(); - map.put("SalesforceOauthReactBridge", new ReactModuleInfo( - "SalesforceOauthReactBridge", "SalesforceOauthReactBridge", false, false, false, true)); - map.put("SalesforceNetReactBridge", new ReactModuleInfo( - "SalesforceNetReactBridge", "SalesforceNetReactBridge", false, false, false, true)); - map.put("SmartStoreReactBridge", new ReactModuleInfo( - "SmartStoreReactBridge", "SmartStoreReactBridge", false, false, false, true)); - map.put("MobileSyncReactBridge", new ReactModuleInfo( - "MobileSyncReactBridge", "MobileSyncReactBridge", false, false, false, true)); - return map; - }; + List modules = new java.util.ArrayList<>(); + modules.add(new SalesforceOauthReactBridge(reactContext)); + modules.add(new SalesforceNetReactBridge(reactContext)); + modules.add(new SmartStoreReactBridge(reactContext)); + modules.add(new MobileSyncReactBridge(reactContext)); + return modules; } @NonNull From baae186fc69cab5e45d83a6398fe90ad605d8ccb Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 22 May 2026 16:08:16 -0700 Subject: [PATCH 6/6] Use named SalesforceReactPackage extending BaseReactPackage Create a proper Kotlin class for the package registration to ensure BaseReactPackage is recognized by ReactPackageTurboModuleManagerDelegate in bridgeless new architecture mode. --- .../reactnative/app/SalesforceReactPackage.kt | 74 +++++++++++++++++++ .../app/SalesforceReactSDKManager.java | 32 +------- 2 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactPackage.kt diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactPackage.kt b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactPackage.kt new file mode 100644 index 0000000000..085b9aab46 --- /dev/null +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactPackage.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024-present, salesforce.com, inc. + * All rights reserved. + * Redistribution and use of this software in source and binary forms, with or + * without modification, are permitted provided that the following conditions + * are met: + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of salesforce.com, inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission of salesforce.com, inc. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.salesforce.androidsdk.reactnative.app + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.uimanager.ViewManager +import com.salesforce.androidsdk.reactnative.bridge.MobileSyncReactBridge +import com.salesforce.androidsdk.reactnative.bridge.SalesforceNetReactBridge +import com.salesforce.androidsdk.reactnative.bridge.SalesforceOauthReactBridge +import com.salesforce.androidsdk.reactnative.bridge.SmartStoreReactBridge + +class SalesforceReactPackage : BaseReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return when (name) { + "SalesforceOauthReactBridge" -> SalesforceOauthReactBridge(reactContext) + "SalesforceNetReactBridge" -> SalesforceNetReactBridge(reactContext) + "SmartStoreReactBridge" -> SmartStoreReactBridge(reactContext) + "MobileSyncReactBridge" -> MobileSyncReactBridge(reactContext) + else -> null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + mapOf( + "SalesforceOauthReactBridge" to ReactModuleInfo( + "SalesforceOauthReactBridge", "SalesforceOauthReactBridge", false, false, false, true + ), + "SalesforceNetReactBridge" to ReactModuleInfo( + "SalesforceNetReactBridge", "SalesforceNetReactBridge", false, false, false, true + ), + "SmartStoreReactBridge" to ReactModuleInfo( + "SmartStoreReactBridge", "SmartStoreReactBridge", false, false, false, true + ), + "MobileSyncReactBridge" to ReactModuleInfo( + "MobileSyncReactBridge", "MobileSyncReactBridge", false, false, false, true + ), + ) + } + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java index 425b0f7327..9cc795c4f1 100644 --- a/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java +++ b/libs/SalesforceReact/src/com/salesforce/androidsdk/reactnative/app/SalesforceReactSDKManager.java @@ -35,20 +35,12 @@ import androidx.annotation.VisibleForTesting; import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager; -import com.salesforce.androidsdk.reactnative.bridge.MobileSyncReactBridge; -import com.salesforce.androidsdk.reactnative.bridge.SalesforceNetReactBridge; -import com.salesforce.androidsdk.reactnative.bridge.SalesforceOauthReactBridge; -import com.salesforce.androidsdk.reactnative.bridge.SmartStoreReactBridge; import com.salesforce.androidsdk.reactnative.ui.SalesforceReactActivity; import com.salesforce.androidsdk.ui.LoginActivity; import com.salesforce.androidsdk.util.EventsObservable; import com.salesforce.androidsdk.util.EventsObservable.EventType; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -134,29 +126,7 @@ public String getAppType() { * @return ReactPackage for this application */ public ReactPackage getReactPackage() { - return new ReactPackage() { - - @NonNull - @Override - public List createNativeModules( - @NonNull ReactApplicationContext reactContext - ) { - List modules = new java.util.ArrayList<>(); - modules.add(new SalesforceOauthReactBridge(reactContext)); - modules.add(new SalesforceNetReactBridge(reactContext)); - modules.add(new SmartStoreReactBridge(reactContext)); - modules.add(new MobileSyncReactBridge(reactContext)); - return modules; - } - - @NonNull - @Override - public List createViewManagers( - @NonNull ReactApplicationContext reactContext - ) { - return Collections.emptyList(); - } - }; + return new SalesforceReactPackage(); } @NonNull