feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,19 @@
apply plugin: 'com.android.library'
group = 'expo.modules.asset'
version = '10.0.10'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useDefaultAndroidSdkVersions()
useExpoPublishing()
android {
namespace "expo.modules.asset"
defaultConfig {
versionCode 1
versionName "10.0.10"
}
}

View File

@@ -0,0 +1,2 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,98 @@
package expo.modules.asset
import android.content.Context
import android.net.Uri
import expo.modules.interfaces.filesystem.Permission
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.net.URI
import java.security.DigestInputStream
import java.security.MessageDigest
internal class UnableToDownloadAssetException(url: String) :
CodedException("Unable to download asset from url: $url")
class AssetModule : Module() {
private val context: Context
get() = appContext.reactContext ?: throw Exceptions.AppContextLost()
private fun getMD5HashOfFilePath(uri: URI): String {
val md = MessageDigest.getInstance("MD5")
return md.digest(uri.toString().toByteArray()).joinToString("") { "%02x".format(it) }
}
private fun getMD5HashOfFileContent(file: File): String? {
return try {
FileInputStream(file).use { inputStream ->
DigestInputStream(
inputStream,
MessageDigest.getInstance("MD5")
).use { digestInputStream ->
digestInputStream.messageDigest.digest().joinToString(separator = "") { "%02x".format(it) }
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private suspend fun downloadAsset(appContext: AppContext, uri: URI, localUrl: File): Uri {
if (localUrl.parentFile?.exists() != true) {
localUrl.mkdirs()
}
if (appContext.filePermission?.getPathPermissions(appContext.reactContext, localUrl.parent)?.contains(Permission.WRITE) != true) {
throw UnableToDownloadAssetException(uri.toString())
}
return withContext(appContext.backgroundCoroutineScope.coroutineContext) {
try {
val inputStream = when {
uri.toString().contains(":").not() -> openAssetResourceStream(context, uri.toString())
else -> uri.toURL().openStream()
}
inputStream.use { input ->
localUrl.outputStream().use { output ->
input.copyTo(output)
}
}
Uri.fromFile(localUrl)
} catch (e: Exception) {
throw UnableToDownloadAssetException(uri.toString())
}
}
}
override fun definition() = ModuleDefinition {
Name("ExpoAsset")
AsyncFunction("downloadAsync") Coroutine { uri: URI, md5Hash: String?, type: String ->
if (uri.scheme === "file") {
return@Coroutine uri
}
val cacheFileId = md5Hash ?: getMD5HashOfFilePath(uri)
val cacheDirectory = appContext.cacheDirectory
val localUrl = File("$cacheDirectory/ExponentAsset-$cacheFileId.$type")
if (!localUrl.exists()) {
return@Coroutine downloadAsset(appContext, uri, localUrl)
}
if (md5Hash == null || md5Hash == getMD5HashOfFileContent(localUrl)) {
return@Coroutine Uri.fromFile(localUrl)
}
return@Coroutine downloadAsset(appContext, uri, localUrl)
}
}
}

View File

@@ -0,0 +1,24 @@
package expo.modules.asset
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import java.io.InputStream
/**
* Opens an Android resource as stream.
*/
internal fun openAssetResourceStream(context: Context, assetName: String): InputStream {
val resources = context.resources
val resId = findResourceId(context, assetName) ?: throw Resources.NotFoundException(assetName)
return resources.openRawResource(resId)
}
@SuppressLint("DiscouragedApi")
private fun findResourceId(context: Context, assetName: String): Int? {
val resources = context.resources
val packageName = context.packageName
// react-native core and expo-assets plugin will put resource in `res/raw` or `res/drawable`
return resources.getIdentifier(assetName, "raw", packageName).takeIf { it != 0 }
?: resources.getIdentifier(assetName, "drawable", packageName).takeIf { it != 0 }
}