leeo-react-native-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+ pkg = JSON.parse(File.read(File.join(__dir__, 'package.json')))
3
+
4
+ Pod::Spec.new do |s|
5
+ s.name = "LeeoSdk"
6
+ s.version = pkg["version"]
7
+ s.summary = pkg["description"]
8
+ s.homepage = pkg["repository"]["url"] rescue ""
9
+ s.license = pkg["license"] rescue "UNLICENSED"
10
+ s.author = pkg["author"] rescue ""
11
+ s.source = { :git => "", :tag => "#{s.version}" }
12
+ s.platforms = { :ios => "13.0" }
13
+ s.source_files = "ios/*.{m,mm,swift,h}"
14
+ s.pod_target_xcconfig = {
15
+ "DEFINES_MODULE" => "YES",
16
+ "SWIFT_OBJC_BRIDGING_HEADER" => "$(PODS_TARGET_SRCROOT)/ios/LeeoSdk-Bridging-Header.h"
17
+ }
18
+ s.requires_arc = true
19
+ s.dependency "React-Core"
20
+ # Leeo iOS SDK (same code as SPM). Install with: npm install leeo-ios-sdk (or add to your app).
21
+ leeo_ios_path = File.directory?(File.join(__dir__, "node_modules/leeo-ios-sdk")) ? "node_modules/leeo-ios-sdk" : "../leeo-ios-sdk"
22
+ s.dependency "LeeoSDK", :path => leeo_ios_path
23
+ s.swift_version = "5.0"
24
+ end
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Leeo React Native SDK
2
+
3
+ React Native bridge for Leeo trip tracking and incident reporting. Same flow as the native [Android](../leeo-android-sdk) and [iOS](../leeo-ios-sdk) SDKs: setup (fleet-service + DriveKit), manual trip (startTrip/stopTrip), shift (startShift/stopShift), incident reporting.
4
+
5
+ - **Test the demo app:** Create a new RN app, add this package (and `leeo-ios-sdk` for iOS), link Android/iOS, copy [example/App.tsx](example/App.tsx), then run. Full steps in [INSTALL_AND_TEST.md](INSTALL_AND_TEST.md).
6
+ - **Clients:** Install via npm or Git, then same link + permissions steps; see [INSTALL_AND_TEST.md](INSTALL_AND_TEST.md) for install and API usage.
7
+
8
+ ## Installation
9
+
10
+ - **Android:** Add `LeeoSdkPackage` to your app. Configure fleet-service base URL (or omit for production). DriveKit is initialized automatically when `setup()` succeeds with a DriveQuant key.
11
+ - **iOS:** Same code as the native [Leeo iOS SDK](https://github.com/leeoinsurance/leeo-ios-sdk): the RN module is a thin bridge that calls the iOS SDK. Install the iOS SDK so the pod can resolve: `npm install leeo-ios-sdk` (or add it as a dependency; the RN package lists it as optional). Run `pod install` in `ios/`. The iOS SDK is built as a CocoaPods pod (same sources as SPM) and depends on `DriveKitTripAnalysis`.
12
+
13
+ ## Android permissions (required for trip recording)
14
+
15
+ Your app must declare and request these at runtime so DriveKit can record trips and run in the background:
16
+
17
+ 1. **AndroidManifest.xml** (in your app’s `android/app/src/main/`):
18
+ - `android.permission.ACCESS_FINE_LOCATION`
19
+ - `android.permission.ACCESS_COARSE_LOCATION`
20
+ - `android.permission.POST_NOTIFICATIONS` (Android 13+)
21
+ - `android.permission.ACTIVITY_RECOGNITION` (Android 10+; “Physical activity”)
22
+
23
+ 2. **Runtime requests** (before calling Setup or Start Trip): request **POST_NOTIFICATIONS** on Android 13+ and **ACTIVITY_RECOGNITION** on Android 10+, e.g. in your root component or MainActivity using `PermissionsAndroid` or a library like `react-native-permissions`. If the user denies notifications or physical activity, trip recording may not work. See the [example README](example/README.md) for a minimal setup.
24
+
25
+ ## API
26
+
27
+ - **Config:** `baseUrl` is optional; defaults to `DEFAULT_BASE_URL` (`https://api.leeoinsurance.com`). Import `DEFAULT_BASE_URL` from the package.
28
+ - `Leeo.setup(config, tripNotification)` – POST fleet-service /fleet/v1/sdk/setup; on success configures DriveKit when telematics_provider returns a DriveQuant key. Same logic as Android/iOS (with 8s connection timeout if DriveKit doesn’t connect).
29
+ - `Leeo.startTrip(trackingId)` / `Leeo.stopTrip()` – manual trip (DriveKit).
30
+ - `Leeo.startShift(shiftId?)` / `Leeo.stopShift()` – shift/auto trip (DriveKit).
31
+ - `Leeo.teardown()` / `Leeo.wipeOut()` – clear state and tear down DriveKit.
32
+ - `Leeo.openIncidentReportingWebPage()` / `Leeo.getIncidentReportingURL()`
33
+
34
+ See [LEEO_SDK_DESIGN.md](../LEEO_SDK_DESIGN.md).
@@ -0,0 +1,20 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+
4
+ android {
5
+ namespace "com.leeo.sdk"
6
+ compileSdk 34
7
+ defaultConfig {
8
+ minSdk 26
9
+ targetSdk 34
10
+ }
11
+ }
12
+
13
+ dependencies {
14
+ implementation "com.facebook.react:react-native:+"
15
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.25"
16
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
17
+ implementation "com.google.code.gson:gson:2.10.1"
18
+ implementation "com.drivequant.drivekit:drivekit-core:2.26.0"
19
+ implementation "com.drivequant.drivekit:drivekit-trip-analysis:2.26.0"
20
+ }
@@ -0,0 +1,105 @@
1
+ package com.leeo.sdk
2
+
3
+ import com.google.gson.Gson
4
+ import com.google.gson.annotations.SerializedName
5
+ import okhttp3.MediaType.Companion.toMediaType
6
+ import okhttp3.OkHttpClient
7
+ import okhttp3.Request
8
+ import okhttp3.RequestBody.Companion.toRequestBody
9
+ import java.util.concurrent.TimeUnit
10
+
11
+ internal object LeeoApi {
12
+ private const val SETUP_PATH = "/fleet/v1/sdk/setup"
13
+ private const val INCIDENT_AUTH_PATH = "/fleet/v1/sdk/incident-auth-url"
14
+ private const val HEADER_DRIVER_TOKEN = "x-driver-token"
15
+ private val gson = Gson()
16
+ private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
17
+ private val client = OkHttpClient.Builder()
18
+ .connectTimeout(30, TimeUnit.SECONDS)
19
+ .readTimeout(30, TimeUnit.SECONDS)
20
+ .build()
21
+
22
+ fun postSetup(baseUrl: String, sdkKey: String, driverId: String, firstName: String, lastName: String, email: String, phoneNumber: String): Pair<Int, String?> {
23
+ val url = baseUrl.trimEnd('/') + SETUP_PATH
24
+ val body = mapOf(
25
+ "sdk_key" to sdkKey,
26
+ "driver_id" to driverId,
27
+ "first_name" to firstName,
28
+ "last_name" to lastName,
29
+ "email" to email,
30
+ "phone_number" to phoneNumber,
31
+ "sdk_family" to "leeo"
32
+ )
33
+ val request = Request.Builder()
34
+ .url(url)
35
+ .post(gson.toJson(body).toRequestBody(jsonMediaType))
36
+ .addHeader("Content-Type", "application/json")
37
+ .build()
38
+ return runCatching {
39
+ val response = client.newCall(request).execute()
40
+ response.code to response.body?.string()
41
+ }.getOrElse { -1 to null }
42
+ }
43
+
44
+ fun getIncidentAuthUrl(baseUrl: String, loginToken: String, platform: String = "android"): Pair<Int, String?> {
45
+ val url = baseUrl.trimEnd('/') + INCIDENT_AUTH_PATH + "?platform=$platform&source=fm_sdk&version=1"
46
+ val request = Request.Builder()
47
+ .url(url)
48
+ .get()
49
+ .addHeader(HEADER_DRIVER_TOKEN, loginToken)
50
+ .build()
51
+ return runCatching {
52
+ val response = client.newCall(request).execute()
53
+ val bodyStr = response.body?.string() ?: return@runCatching response.code to null
54
+ if (!response.isSuccessful) return@runCatching response.code to null
55
+ val json = gson.fromJson(bodyStr, IncidentAuthResponse::class.java)
56
+ val urlFromData = (json?.data as? Map<*, *>)?.get("claims_incident_auth_url") as? String
57
+ response.code to urlFromData
58
+ }.getOrElse { -1 to null }
59
+ }
60
+
61
+ private class IncidentAuthResponse(val data: Any?)
62
+ }
63
+
64
+ internal object LeeoSetupState {
65
+ @Volatile var loginToken: String? = null
66
+ @Volatile var lastBaseUrl: String? = null
67
+ @Volatile var driveQuantSdkKey: String? = null
68
+ @Volatile var isSetup: Boolean = false
69
+
70
+ fun setFromResponse(json: String, baseUrl: String): Boolean {
71
+ return try {
72
+ // Backend returns { "success": true, "data": { "user_info", "telematics_provider", ... } }
73
+ val response = gson.fromJson(json, SetupResponse::class.java)
74
+ val data = response.data
75
+ if (response.success == true && data?.userInfo?.loginToken != null) {
76
+ loginToken = data.userInfo.loginToken
77
+ lastBaseUrl = baseUrl
78
+ driveQuantSdkKey = data.telematicsProvider?.driveQuant?.sdkKey
79
+ isSetup = true
80
+ true
81
+ } else false
82
+ } catch (_: Exception) {
83
+ false
84
+ }
85
+ }
86
+
87
+ fun clear() {
88
+ loginToken = null
89
+ lastBaseUrl = null
90
+ driveQuantSdkKey = null
91
+ isSetup = false
92
+ }
93
+
94
+ private data class SetupResponse(
95
+ val success: Boolean?,
96
+ val data: SetupData?
97
+ )
98
+ private data class SetupData(
99
+ @SerializedName("user_info") val userInfo: UserInfo?,
100
+ @SerializedName("telematics_provider") val telematicsProvider: TelematicsProvider?
101
+ )
102
+ private data class UserInfo(@SerializedName("login_token") val loginToken: String?)
103
+ private data class TelematicsProvider(@SerializedName("drive_quant") val driveQuant: DriveQuant?)
104
+ private data class DriveQuant(@SerializedName("sdk_key") val sdkKey: String?)
105
+ }
@@ -0,0 +1,122 @@
1
+ package com.leeo.sdk
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import com.drivequant.drivekit.core.DriveKit
8
+ import com.drivequant.drivekit.core.driver.UpdateUserIdStatus
9
+ import com.drivequant.drivekit.core.driver.deletion.DeleteAccountStatus
10
+ import com.drivequant.drivekit.core.networking.*
11
+ import com.drivequant.drivekit.tripanalysis.DriveKitTripAnalysis
12
+ import com.drivequant.drivekit.tripanalysis.entity.TripNotification
13
+
14
+ /**
15
+ * Bridges Leeo React Native module to DriveKit. Call [ensureInitialized] before [onSetupSuccess].
16
+ */
17
+ internal object LeeoDriveKitBridge : DriveKitListener {
18
+
19
+ private val mainHandler = Handler(Looper.getMainLooper())
20
+
21
+ /** Initialize DriveKit from app context. Call from setup before configuring key. Returns true if initialized. */
22
+ fun ensureInitialized(context: Context): Boolean {
23
+ val app = context.applicationContext as? Application ?: return false
24
+ if (!DriveKit.isInitialized()) {
25
+ DriveKit.initialize(app)
26
+ DriveKitTripAnalysis.initialize(TripNotification("Trip", "Recording...", 0))
27
+ }
28
+ return true
29
+ }
30
+
31
+ @Volatile
32
+ private var pendingSetupCallback: ((Boolean, String?) -> Unit)? = null
33
+
34
+ private var setupTimeoutRunnable: Runnable? = null
35
+
36
+ /** Timeout (ms) after which we call callback(true) if DriveKit hasn't connected (e.g. in emulator). */
37
+ private const val SETUP_CONNECTION_TIMEOUT_MS = 8_000L
38
+
39
+ fun onSetupSuccess(
40
+ driveQuantSdkKey: String,
41
+ driverId: String,
42
+ callback: (Boolean, String?) -> Unit,
43
+ ) {
44
+ if (!DriveKit.isInitialized()) {
45
+ mainHandler.post { callback(false, "DriveKit not initialized. Call LeeoSdk.initialize() at app startup.") }
46
+ return
47
+ }
48
+ pendingSetupCallback = callback
49
+ DriveKit.addDriveKitListener(this)
50
+ DriveKit.setApiKey(driveQuantSdkKey)
51
+ DriveKitTripAnalysis.setStopTimeOut(Int.MAX_VALUE)
52
+ DriveKit.setUserId(driverId)
53
+ val timeoutRunnable = Runnable {
54
+ val cb = pendingSetupCallback
55
+ if (cb != null) {
56
+ pendingSetupCallback = null
57
+ setupTimeoutRunnable = null
58
+ DriveKit.removeDriveKitListener(this)
59
+ cb(true, null)
60
+ }
61
+ }
62
+ setupTimeoutRunnable = timeoutRunnable
63
+ mainHandler.postDelayed(timeoutRunnable, SETUP_CONNECTION_TIMEOUT_MS)
64
+ }
65
+
66
+ override fun onConnected() {
67
+ mainHandler.post {
68
+ setupTimeoutRunnable?.let { mainHandler.removeCallbacks(it) }
69
+ setupTimeoutRunnable = null
70
+ DriveKit.removeDriveKitListener(this)
71
+ pendingSetupCallback?.invoke(true, null)
72
+ pendingSetupCallback = null
73
+ }
74
+ }
75
+
76
+ override fun onDisconnected() {}
77
+
78
+ override fun onAuthenticationError(errorType: RequestError) {}
79
+
80
+ override fun userIdUpdateStatus(status: UpdateUserIdStatus, userId: String?) {}
81
+
82
+ override fun onAccountDeleted(status: DeleteAccountStatus) {}
83
+
84
+ fun tearDown(callback: () -> Unit) {
85
+ if (!DriveKit.isInitialized()) {
86
+ mainHandler.post(callback)
87
+ return
88
+ }
89
+ if (DriveKitTripAnalysis.isTripRunning()) {
90
+ DriveKitTripAnalysis.stopTrip()
91
+ DriveKitTripAnalysis.activateAutoStart(false)
92
+ }
93
+ DriveKit.reset()
94
+ mainHandler.post(callback)
95
+ }
96
+
97
+ fun startTrip(trackingId: String): Boolean {
98
+ if (!DriveKit.isInitialized() || !DriveKit.isUserConnected()) return false
99
+ DriveKitTripAnalysis.updateTripMetaData("tracking_id", trackingId)
100
+ DriveKitTripAnalysis.startTrip()
101
+ return true
102
+ }
103
+
104
+ fun stopTrip(): Boolean {
105
+ if (!DriveKit.isInitialized()) return false
106
+ DriveKitTripAnalysis.stopTrip()
107
+ return true
108
+ }
109
+
110
+ fun startShift(): Boolean {
111
+ if (!DriveKit.isInitialized() || !DriveKit.isUserConnected()) return false
112
+ DriveKitTripAnalysis.activateAutoStart(true)
113
+ return true
114
+ }
115
+
116
+ fun stopShift(): Boolean {
117
+ if (!DriveKit.isInitialized()) return false
118
+ if (DriveKitTripAnalysis.isTripRunning()) DriveKitTripAnalysis.stopTrip()
119
+ DriveKitTripAnalysis.activateAutoStart(false)
120
+ return true
121
+ }
122
+ }
@@ -0,0 +1,182 @@
1
+ package com.leeo.sdk
2
+
3
+ import android.content.Intent
4
+ import android.net.Uri
5
+ import com.facebook.react.bridge.*
6
+ import java.util.concurrent.Executors
7
+
8
+ class LeeoSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
9
+
10
+ private val executor = Executors.newSingleThreadExecutor()
11
+
12
+ override fun getName(): String = "LeeoSdk"
13
+
14
+ @ReactMethod
15
+ fun setup(config: ReadableMap, tripNotification: ReadableMap, promise: Promise) {
16
+ val baseUrl = config.getString("baseUrl") ?: run {
17
+ promise.resolve(errorMap("INVALID_DRIVER_ID", "baseUrl is required"))
18
+ return
19
+ }
20
+ val sdkKey = config.getString("sdkKey") ?: ""
21
+ val driverId = config.getString("driverId") ?: ""
22
+ val attrs = config.getMap("driverAttributes") ?: run {
23
+ promise.resolve(errorMap("INVALID_DRIVER_ID", "driverAttributes required"))
24
+ return
25
+ }
26
+ val firstName = (attrs.getString("firstName") ?: "").trim()
27
+ val lastName = (attrs.getString("lastName") ?: "").trim()
28
+ if (firstName.isBlank() || lastName.isBlank()) {
29
+ promise.resolve(errorMap("INVALID_DRIVER_ID", "first_name and last_name required"))
30
+ return
31
+ }
32
+ val email = attrs.getString("email") ?: ""
33
+ val phoneNumber = attrs.getString("phoneNumber") ?: ""
34
+ executor.execute {
35
+ val (code, body) = LeeoApi.postSetup(baseUrl, sdkKey, driverId, firstName, lastName, email, phoneNumber)
36
+ runOnReactQueue {
37
+ when {
38
+ code in 200..299 && body != null && LeeoSetupState.setFromResponse(body, baseUrl) -> {
39
+ val dqKey = LeeoSetupState.driveQuantSdkKey
40
+ if (!dqKey.isNullOrBlank()) {
41
+ if (!LeeoDriveKitBridge.ensureInitialized(reactApplicationContext)) {
42
+ promise.resolve(errorMap("LEEO_SDK_ERROR", "DriveKit could not be initialized"))
43
+ return@runOnReactQueue
44
+ }
45
+ LeeoDriveKitBridge.onSetupSuccess(dqKey, driverId) { success, errorMsg ->
46
+ runOnReactQueue {
47
+ if (success) promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
48
+ else promise.resolve(errorMap("LEEO_SDK_ERROR", errorMsg ?: "DriveKit setup failed"))
49
+ }
50
+ }
51
+ } else {
52
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
53
+ }
54
+ }
55
+ code == 404 -> {
56
+ val msg = body ?: "Not found"
57
+ val isDriverNotFound = msg.contains("Driver", ignoreCase = true) && msg.contains("not found", ignoreCase = true)
58
+ val codeStr = if (isDriverNotFound) "DRIVER_NOT_FOUND" else "INVALID_OR_NOT_FOUND_SDK_KEY"
59
+ promise.resolve(errorMap(codeStr, msg))
60
+ }
61
+ code == 400 -> promise.resolve(errorMap("INVALID_DRIVER_ID", body ?: "Bad request"))
62
+ else -> promise.resolve(errorMap("NETWORK_NOT_AVAILABLE", body ?: "Setup failed (code $code)"))
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ private fun errorMap(code: String, message: String): WritableMap =
69
+ Arguments.createMap().apply { putBoolean("success", false); putString("errorCode", code); putString("message", message) }
70
+
71
+ private fun runOnReactQueue(r: () -> Unit) {
72
+ reactApplicationContext.runOnUiQueueThread(r)
73
+ }
74
+
75
+ @ReactMethod
76
+ fun teardown(promise: Promise) {
77
+ LeeoDriveKitBridge.tearDown {
78
+ runOnReactQueue {
79
+ LeeoSetupState.clear()
80
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
81
+ }
82
+ }
83
+ }
84
+
85
+ @ReactMethod
86
+ fun startTrip(trackingId: String, promise: Promise) {
87
+ if (!LeeoSetupState.isSetup) {
88
+ promise.resolve(errorMap("SDK_NOT_SETUP", "Call setup first"))
89
+ return
90
+ }
91
+ val ok = LeeoDriveKitBridge.startTrip(trackingId)
92
+ if (ok) promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
93
+ else promise.resolve(errorMap("LEEO_SDK_ERROR", "DriveKit not ready"))
94
+ }
95
+
96
+ @ReactMethod
97
+ fun stopTrip(promise: Promise) {
98
+ val ok = LeeoDriveKitBridge.stopTrip()
99
+ promise.resolve(if (ok) Arguments.createMap().apply { putBoolean("success", true) } else errorMap("LEEO_SDK_ERROR", "DriveKit not initialized"))
100
+ }
101
+
102
+ @ReactMethod
103
+ fun startShift(shiftId: String?, promise: Promise) {
104
+ if (!LeeoSetupState.isSetup) {
105
+ promise.resolve(errorMap("SDK_NOT_SETUP", "Call setup first"))
106
+ return
107
+ }
108
+ val ok = LeeoDriveKitBridge.startShift()
109
+ if (ok) promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
110
+ else promise.resolve(errorMap("LEEO_SDK_ERROR", "DriveKit not ready"))
111
+ }
112
+
113
+ @ReactMethod
114
+ fun stopShift(promise: Promise) {
115
+ val ok = LeeoDriveKitBridge.stopShift()
116
+ promise.resolve(if (ok) Arguments.createMap().apply { putBoolean("success", true) } else errorMap("LEEO_SDK_ERROR", "DriveKit not initialized"))
117
+ }
118
+
119
+ @ReactMethod
120
+ fun wipeOut(promise: Promise) {
121
+ LeeoDriveKitBridge.tearDown {
122
+ runOnReactQueue {
123
+ LeeoSetupState.clear()
124
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
125
+ }
126
+ }
127
+ }
128
+
129
+ @ReactMethod
130
+ fun getBuildVersion(promise: Promise) {
131
+ promise.resolve("0.1.0")
132
+ }
133
+
134
+ @ReactMethod
135
+ fun openIncidentReportingWebPage(promise: Promise) {
136
+ val loginToken = LeeoSetupState.loginToken
137
+ val baseUrl = LeeoSetupState.lastBaseUrl
138
+ if (loginToken == null || baseUrl == null) {
139
+ promise.resolve(errorMap("SDK_NOT_SETUP", "Call setup first"))
140
+ return
141
+ }
142
+ executor.execute {
143
+ val (code, url) = LeeoApi.getIncidentAuthUrl(baseUrl, loginToken)
144
+ runOnReactQueue {
145
+ when {
146
+ code == 200 && !url.isNullOrBlank() -> {
147
+ try {
148
+ currentActivity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
149
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
150
+ } catch (e: Exception) {
151
+ promise.resolve(errorMap("LEEO_SDK_ERROR", e.message ?: "Could not open browser"))
152
+ }
153
+ }
154
+ code == 404 -> promise.resolve(errorMap("INVALID_OR_NOT_FOUND_SDK_KEY", "Driver not found"))
155
+ else -> promise.resolve(errorMap("NETWORK_NOT_AVAILABLE", "Failed to get incident URL"))
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ @ReactMethod
162
+ fun getIncidentReportingURL(promise: Promise) {
163
+ val loginToken = LeeoSetupState.loginToken
164
+ val baseUrl = LeeoSetupState.lastBaseUrl
165
+ if (loginToken == null || baseUrl == null) {
166
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", false); putString("errorCode", "SDK_NOT_SETUP") })
167
+ return
168
+ }
169
+ executor.execute {
170
+ val (code, url) = LeeoApi.getIncidentAuthUrl(baseUrl, loginToken)
171
+ runOnReactQueue {
172
+ if (code == 200 && url != null) {
173
+ promise.resolve(url)
174
+ } else if (code == 404) {
175
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", false); putString("errorCode", "INVALID_OR_NOT_FOUND_SDK_KEY") })
176
+ } else {
177
+ promise.resolve(Arguments.createMap().apply { putBoolean("success", false); putString("errorCode", "NETWORK_NOT_AVAILABLE") })
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
@@ -0,0 +1,16 @@
1
+ package com.leeo.sdk
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class LeeoSdkPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(LeeoSdkModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ #import <React/RCTBridgeModule.h>
package/ios/LeeoSdk.mm ADDED
@@ -0,0 +1,44 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(LeeoSdk, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(setup:(NSDictionary *)config
6
+ tripNotification:(NSDictionary *)tripNotification
7
+ resolver:(RCTPromiseResolveBlock)resolve
8
+ rejecter:(RCTPromiseRejectBlock)reject)
9
+
10
+ RCT_EXTERN_METHOD(teardown:(RCTPromiseResolveBlock)resolve
11
+ rejecter:(RCTPromiseRejectBlock)reject)
12
+
13
+ RCT_EXTERN_METHOD(startTrip:(NSString *)trackingId
14
+ resolver:(RCTPromiseResolveBlock)resolve
15
+ rejecter:(RCTPromiseRejectBlock)reject)
16
+
17
+ RCT_EXTERN_METHOD(stopTrip:(RCTPromiseResolveBlock)resolve
18
+ rejecter:(RCTPromiseRejectBlock)reject)
19
+
20
+ RCT_EXTERN_METHOD(startShift:(NSString *)shiftId
21
+ resolver:(RCTPromiseResolveBlock)resolve
22
+ rejecter:(RCTPromiseRejectBlock)reject)
23
+
24
+ RCT_EXTERN_METHOD(stopShift:(RCTPromiseResolveBlock)resolve
25
+ rejecter:(RCTPromiseRejectBlock)reject)
26
+
27
+ RCT_EXTERN_METHOD(wipeOut:(RCTPromiseResolveBlock)resolve
28
+ rejecter:(RCTPromiseRejectBlock)reject)
29
+
30
+ RCT_EXTERN_METHOD(getBuildVersion:(RCTPromiseResolveBlock)resolve
31
+ rejecter:(RCTPromiseRejectBlock)reject)
32
+
33
+ RCT_EXTERN_METHOD(openIncidentReportingWebPage:(RCTPromiseResolveBlock)resolve
34
+ rejecter:(RCTPromiseRejectBlock)reject)
35
+
36
+ RCT_EXTERN_METHOD(getIncidentReportingURL:(RCTPromiseResolveBlock)resolve
37
+ rejecter:(RCTPromiseRejectBlock)reject)
38
+
39
+ + (BOOL)requiresMainQueueSetup
40
+ {
41
+ return NO;
42
+ }
43
+
44
+ @end
@@ -0,0 +1,169 @@
1
+ //
2
+ // Leeo React Native SDK – iOS thin bridge to LeeoSDK (same as native iOS SDK).
3
+ // Depends on LeeoSDK pod (CocoaPods build of leeo-ios-sdk).
4
+ //
5
+
6
+ import Foundation
7
+ import UIKit
8
+ import LeeoSDK
9
+
10
+ @objc(LeeoSdk)
11
+ class LeeoSdk: NSObject {
12
+
13
+ @objc static func requiresMainQueueSetup() -> Bool { false }
14
+
15
+ private static func success() -> [String: Any] { ["success": true] }
16
+ private static func error(_ code: String, _ message: String) -> [String: Any] {
17
+ ["success": false, "errorCode": code, "message": message]
18
+ }
19
+
20
+ private static func config(from configDict: NSDictionary, tripNotificationDict: NSDictionary) -> (LeeoConfiguration, LeeoTripNotification)? {
21
+ guard let baseUrl = configDict["baseUrl"] as? String, !baseUrl.isEmpty,
22
+ let sdkKey = configDict["sdkKey"] as? String,
23
+ let driverId = configDict["driverId"] as? String,
24
+ let attrs = configDict["driverAttributes"] as? NSDictionary,
25
+ let firstName = (attrs["firstName"] as? String)?.trimmingCharacters(in: .whitespaces),
26
+ let lastName = (attrs["lastName"] as? String)?.trimmingCharacters(in: .whitespaces),
27
+ !firstName.isEmpty, !lastName.isEmpty else { return nil }
28
+ let email = attrs["email"] as? String ?? ""
29
+ let phoneNumber = attrs["phoneNumber"] as? String ?? ""
30
+ let configuration = LeeoConfiguration(
31
+ baseUrl: baseUrl,
32
+ sdkKey: sdkKey,
33
+ driverId: driverId,
34
+ driverAttributes: LeeoDriverAttributes(
35
+ firstName: firstName,
36
+ lastName: lastName,
37
+ email: email,
38
+ phoneNumber: phoneNumber
39
+ )
40
+ )
41
+ let title = (tripNotificationDict["title"] as? String) ?? "Trip"
42
+ let content = (tripNotificationDict["content"] as? String) ?? "Recording..."
43
+ let iconId = (tripNotificationDict["iconId"] as? NSNumber)?.intValue ?? 0
44
+ let notification = LeeoTripNotification(title: title, content: content, iconId: iconId)
45
+ return (configuration, notification)
46
+ }
47
+
48
+ private static func errorCode(from error: Error?) -> (String, String) {
49
+ guard let e = error else { return ("LEEO_SDK_ERROR", "Unknown error") }
50
+ if let leeoError = e as? LeeoError {
51
+ switch leeoError {
52
+ case .sdkNotSetup: return ("SDK_NOT_SETUP", "Call setup first")
53
+ case .invalidOrNotFoundSdkKey: return ("INVALID_OR_NOT_FOUND_SDK_KEY", e.localizedDescription)
54
+ case .invalidDriverId: return ("INVALID_DRIVER_ID", e.localizedDescription)
55
+ case .networkNotAvailable: return ("NETWORK_NOT_AVAILABLE", e.localizedDescription)
56
+ case .leeoSDKError(let msg): return ("LEEO_SDK_ERROR", msg)
57
+ default: return ("LEEO_SDK_ERROR", e.localizedDescription)
58
+ }
59
+ }
60
+ return ("LEEO_SDK_ERROR", e.localizedDescription)
61
+ }
62
+
63
+ @objc func setup(
64
+ _ config: NSDictionary,
65
+ tripNotification: NSDictionary,
66
+ resolver resolve: @escaping RCTPromiseResolveBlock,
67
+ rejecter _: RCTPromiseRejectBlock
68
+ ) {
69
+ guard let (configuration, notification) = Self.config(from: config, tripNotificationDict: tripNotification) else {
70
+ resolve(Self.error("INVALID_DRIVER_ID", "baseUrl, driverAttributes (firstName, lastName) required"))
71
+ return
72
+ }
73
+ Leeo.setup(configuration: configuration, tripNotification: notification) { success, error in
74
+ DispatchQueue.main.async {
75
+ if success { resolve(Self.success()) }
76
+ else {
77
+ let (code, msg) = Self.errorCode(from: error)
78
+ resolve(Self.error(code, msg))
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ @objc func teardown(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
85
+ Leeo.teardown { DispatchQueue.main.async { resolve(Self.success()) } }
86
+ }
87
+
88
+ @objc func startTrip(_ trackingId: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
89
+ Leeo.startTrip(trackingId: trackingId ?? "") { success, error in
90
+ DispatchQueue.main.async {
91
+ if success { resolve(Self.success()) }
92
+ else {
93
+ let (code, msg) = Self.errorCode(from: error)
94
+ resolve(Self.error(code, msg))
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ @objc func stopTrip(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
101
+ Leeo.stopTrip { success, error in
102
+ DispatchQueue.main.async {
103
+ if success { resolve(Self.success()) }
104
+ else {
105
+ let (code, msg) = Self.errorCode(from: error)
106
+ resolve(Self.error(code, msg))
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ @objc func startShift(_ shiftId: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
113
+ Leeo.startShift(shiftId: shiftId) { success, error in
114
+ DispatchQueue.main.async {
115
+ if success { resolve(Self.success()) }
116
+ else {
117
+ let (code, msg) = Self.errorCode(from: error)
118
+ resolve(Self.error(code, msg))
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ @objc func stopShift(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
125
+ Leeo.stopShift { success, error in
126
+ DispatchQueue.main.async {
127
+ if success { resolve(Self.success()) }
128
+ else {
129
+ let (code, msg) = Self.errorCode(from: error)
130
+ resolve(Self.error(code, msg))
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ @objc func wipeOut(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
137
+ Leeo.wipeOut { _ in DispatchQueue.main.async { resolve(Self.success()) } }
138
+ }
139
+
140
+ @objc func getBuildVersion(_ resolve: RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
141
+ resolve(Leeo.buildVersion)
142
+ }
143
+
144
+ @objc func openIncidentReportingWebPage(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
145
+ Leeo.openIncidentReportingWebPage { error in
146
+ DispatchQueue.main.async {
147
+ if error == nil { resolve(Self.success()) }
148
+ else {
149
+ let (code, msg) = Self.errorCode(from: error)
150
+ resolve(Self.error(code, msg))
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ @objc func getIncidentReportingURL(_ resolve: @escaping RCTPromiseResolveBlock, rejecter _: RCTPromiseRejectBlock) {
157
+ Leeo.getIncidentReportingURL { url, error in
158
+ DispatchQueue.main.async {
159
+ if let url = url { resolve(url) }
160
+ else if let error = error {
161
+ let (code, _) = Self.errorCode(from: error)
162
+ resolve(["success": false, "errorCode": code])
163
+ } else {
164
+ resolve(["success": false, "errorCode": "SDK_NOT_SETUP"])
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Leeo React Native SDK
3
+ *
4
+ * Two flows: Manual (startTrip/stopTrip) and Shift (startShift/stopShift).
5
+ * Setup calls fleet-service: POST /fleet/v1/sdk/setup with sdk_key in body.
6
+ */
7
+ /** Production API base URL. Omit or leave empty in config to use this. */
8
+ export declare const DEFAULT_BASE_URL = "https://api.leeoinsurance.com";
9
+ export interface LeeoConfiguration {
10
+ /** Fleet-service base URL. Defaults to [DEFAULT_BASE_URL]. No trailing slash. */
11
+ baseUrl?: string;
12
+ sdkKey: string;
13
+ driverId: string;
14
+ driverAttributes: {
15
+ firstName: string;
16
+ lastName: string;
17
+ email: string;
18
+ phoneNumber: string;
19
+ };
20
+ }
21
+ export interface LeeoTripNotification {
22
+ title: string;
23
+ content: string;
24
+ iconId?: number;
25
+ }
26
+ export type LeeoOperationResult = {
27
+ success: true;
28
+ } | {
29
+ success: false;
30
+ errorCode: string;
31
+ message: string;
32
+ };
33
+ export declare const Leeo: {
34
+ setup(config: LeeoConfiguration, tripNotification: LeeoTripNotification): Promise<LeeoOperationResult>;
35
+ teardown(): Promise<LeeoOperationResult>;
36
+ startTrip(trackingId: string): Promise<LeeoOperationResult>;
37
+ stopTrip(): Promise<LeeoOperationResult>;
38
+ startShift(shiftId?: string): Promise<LeeoOperationResult>;
39
+ stopShift(): Promise<LeeoOperationResult>;
40
+ wipeOut(): Promise<LeeoOperationResult>;
41
+ getBuildVersion(): string;
42
+ openIncidentReportingWebPage(): Promise<LeeoOperationResult>;
43
+ getIncidentReportingURL(): Promise<string | {
44
+ success: false;
45
+ errorCode: string;
46
+ }>;
47
+ };
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Leeo React Native SDK
4
+ *
5
+ * Two flows: Manual (startTrip/stopTrip) and Shift (startShift/stopShift).
6
+ * Setup calls fleet-service: POST /fleet/v1/sdk/setup with sdk_key in body.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Leeo = exports.DEFAULT_BASE_URL = void 0;
10
+ const react_native_1 = require("react-native");
11
+ const { LeeoSdk } = react_native_1.NativeModules;
12
+ /** Production API base URL. Omit or leave empty in config to use this. */
13
+ exports.DEFAULT_BASE_URL = 'https://api.leeoinsurance.com';
14
+ function normalizeConfig(config) {
15
+ const baseUrl = (config.baseUrl ?? '').trim();
16
+ return {
17
+ ...config,
18
+ baseUrl: baseUrl || exports.DEFAULT_BASE_URL,
19
+ };
20
+ }
21
+ exports.Leeo = {
22
+ setup(config, tripNotification) {
23
+ const normalized = normalizeConfig(config);
24
+ return LeeoSdk?.setup?.(normalized, tripNotification) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Native module not linked' });
25
+ },
26
+ teardown() {
27
+ return LeeoSdk?.teardown?.() ?? Promise.resolve({ success: true });
28
+ },
29
+ startTrip(trackingId) {
30
+ return LeeoSdk?.startTrip?.(trackingId) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
31
+ },
32
+ stopTrip() {
33
+ return LeeoSdk?.stopTrip?.() ?? Promise.resolve({ success: true });
34
+ },
35
+ startShift(shiftId) {
36
+ return LeeoSdk?.startShift?.(shiftId) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
37
+ },
38
+ stopShift() {
39
+ return LeeoSdk?.stopShift?.() ?? Promise.resolve({ success: true });
40
+ },
41
+ wipeOut() {
42
+ return LeeoSdk?.wipeOut?.() ?? Promise.resolve({ success: true });
43
+ },
44
+ getBuildVersion() {
45
+ return LeeoSdk?.getBuildVersion?.() ?? '0.1.0';
46
+ },
47
+ openIncidentReportingWebPage() {
48
+ return LeeoSdk?.openIncidentReportingWebPage?.() ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
49
+ },
50
+ getIncidentReportingURL() {
51
+ return LeeoSdk?.getIncidentReportingURL?.() ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED' });
52
+ },
53
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "leeo-react-native-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Leeo SDK for React Native – trip tracking and incident reporting",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "scripts": {
11
+ "build": "npx tsc",
12
+ "prepare": "npm run build"
13
+ },
14
+ "keywords": ["react-native", "leeo", "trip", "drivequant"],
15
+ "repository": { "type": "git", "url": "" },
16
+ "author": "",
17
+ "license": "UNLICENSED",
18
+ "devDependencies": {
19
+ "react-native": "*",
20
+ "typescript": "^5.0.0"
21
+ },
22
+ "peerDependencies": {
23
+ "react-native": "*"
24
+ },
25
+ "optionalDependencies": {
26
+ "leeo-ios-sdk": "*"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "ios",
31
+ "android",
32
+ "LeeoSdk.podspec",
33
+ "lib"
34
+ ]
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Leeo React Native SDK
3
+ *
4
+ * Two flows: Manual (startTrip/stopTrip) and Shift (startShift/stopShift).
5
+ * Setup calls fleet-service: POST /fleet/v1/sdk/setup with sdk_key in body.
6
+ */
7
+
8
+ import { NativeModules } from 'react-native';
9
+
10
+ const { LeeoSdk } = NativeModules;
11
+
12
+ /** Production API base URL. Omit or leave empty in config to use this. */
13
+ export const DEFAULT_BASE_URL = 'https://api.leeoinsurance.com';
14
+
15
+ export interface LeeoConfiguration {
16
+ /** Fleet-service base URL. Defaults to [DEFAULT_BASE_URL]. No trailing slash. */
17
+ baseUrl?: string;
18
+ sdkKey: string;
19
+ driverId: string;
20
+ driverAttributes: {
21
+ firstName: string;
22
+ lastName: string;
23
+ email: string;
24
+ phoneNumber: string;
25
+ };
26
+ }
27
+
28
+ export interface LeeoTripNotification {
29
+ title: string;
30
+ content: string;
31
+ iconId?: number;
32
+ }
33
+
34
+ export type LeeoOperationResult = { success: true } | { success: false; errorCode: string; message: string };
35
+
36
+ function normalizeConfig(config: LeeoConfiguration): LeeoConfiguration & { baseUrl: string } {
37
+ const baseUrl = (config.baseUrl ?? '').trim();
38
+ return {
39
+ ...config,
40
+ baseUrl: baseUrl || DEFAULT_BASE_URL,
41
+ };
42
+ }
43
+
44
+ export const Leeo = {
45
+ setup(
46
+ config: LeeoConfiguration,
47
+ tripNotification: LeeoTripNotification
48
+ ): Promise<LeeoOperationResult> {
49
+ const normalized = normalizeConfig(config);
50
+ return LeeoSdk?.setup?.(normalized, tripNotification) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Native module not linked' });
51
+ },
52
+
53
+ teardown(): Promise<LeeoOperationResult> {
54
+ return LeeoSdk?.teardown?.() ?? Promise.resolve({ success: true });
55
+ },
56
+
57
+ startTrip(trackingId: string): Promise<LeeoOperationResult> {
58
+ return LeeoSdk?.startTrip?.(trackingId) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
59
+ },
60
+
61
+ stopTrip(): Promise<LeeoOperationResult> {
62
+ return LeeoSdk?.stopTrip?.() ?? Promise.resolve({ success: true });
63
+ },
64
+
65
+ startShift(shiftId?: string): Promise<LeeoOperationResult> {
66
+ return LeeoSdk?.startShift?.(shiftId) ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
67
+ },
68
+
69
+ stopShift(): Promise<LeeoOperationResult> {
70
+ return LeeoSdk?.stopShift?.() ?? Promise.resolve({ success: true });
71
+ },
72
+
73
+ wipeOut(): Promise<LeeoOperationResult> {
74
+ return LeeoSdk?.wipeOut?.() ?? Promise.resolve({ success: true });
75
+ },
76
+
77
+ getBuildVersion(): string {
78
+ return LeeoSdk?.getBuildVersion?.() ?? '0.1.0';
79
+ },
80
+
81
+ openIncidentReportingWebPage(): Promise<LeeoOperationResult> {
82
+ return LeeoSdk?.openIncidentReportingWebPage?.() ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED', message: 'Not implemented' });
83
+ },
84
+
85
+ getIncidentReportingURL(): Promise<string | { success: false; errorCode: string }> {
86
+ return LeeoSdk?.getIncidentReportingURL?.() ?? Promise.resolve({ success: false, errorCode: 'NOT_IMPLEMENTED' });
87
+ },
88
+ };