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 2c89611b2d..25292cbb85 100644 --- a/libs/SalesforceReact/build.gradle.kts +++ b/libs/SalesforceReact/build.gradle.kts @@ -21,9 +21,13 @@ plugins { id("org.jetbrains.dokka") } +kotlin { + jvmToolchain(17) +} + 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) 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", 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 c147d608cb..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,22 +35,12 @@ import androidx.annotation.VisibleForTesting; 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.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.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -136,34 +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 ArrayList<>(); - modules.add(new SalesforceOauthReactBridge(reactContext)); - modules.add(new SalesforceNetReactBridge(reactContext)); - modules.add(new SmartStoreReactBridge(reactContext)); - modules.add(new MobileSyncReactBridge(reactContext)); - return modules; - } - - /** @noinspection unused*/ - public List> createJSModules() { - return Collections.emptyList(); - } - - @NonNull - @Override - public List createViewManagers( - @NonNull ReactApplicationContext reactContext - ) { - return Collections.emptyList(); - } - }; + return new SalesforceReactPackage(); } @NonNull 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"); } } }