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,32 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoSecureStore'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platform = :ios, '13.4'
s.swift_version = '5.4'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
s.source_files = "**/*.h"
s.vendored_frameworks = "#{s.name}.xcframework"
else
s.source_files = "**/*.{h,m,swift}"
end
end

View File

@@ -0,0 +1,11 @@
import ExpoModulesCore
enum SecureStoreAccessible: Int, Enumerable {
case afterFirstUnlock = 0
case afterFirstUnlockThisDeviceOnly = 1
case always = 2
case whenPasscodeSetThisDeviceOnly = 3
case alwaysThisDeviceOnly = 4
case whenUnlocked = 5
case whenUnlockedThisDeviceOnly = 6
}

View File

@@ -0,0 +1,64 @@
import ExpoModulesCore
internal class InvalidKeyException: Exception {
override var reason: String {
"Invalid key"
}
}
internal class MissingPlistKeyException: Exception {
override var reason: String {
"You must set `NSFaceIDUsageDescription` in your Info.plist file to use the `requireAuthentication` option"
}
}
internal class KeyChainException: GenericException<OSStatus> {
override var reason: String {
switch param {
case errSecUnimplemented:
return "Function or operation not implemented."
case errSecIO:
return "I/O error."
case errSecOpWr:
return "File already open with with write permission."
case errSecParam:
return "One or more parameters passed to a function where not valid."
case errSecAllocate:
return "Failed to allocate memory."
case errSecUserCanceled:
return "User canceled the operation."
case errSecBadReq:
return "Bad parameter or invalid state for operation."
case errSecNotAvailable:
return "No keychain is available. You may need to restart your computer."
case errSecDuplicateItem:
return "The specified item already exists in the keychain."
case errSecItemNotFound:
return "The specified item could not be found in the keychain."
case errSecInteractionNotAllowed:
return "User interaction is not allowed."
case errSecDecode:
return "Unable to decode the provided data."
case errSecAuthFailed:
return "Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device."
default:
if let errorMessage = SecCopyErrorMessageString(param, nil) as? String {
return errorMessage
}
return "Unknown Keychain Error."
}
}
}

View File

@@ -0,0 +1,207 @@
import ExpoModulesCore
import LocalAuthentication
import Security
public final class SecureStoreModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSecureStore")
Constants([
"AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue,
"AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue,
"ALWAYS": SecureStoreAccessible.always.rawValue,
"WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue,
"ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue,
"WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue,
"WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue
])
AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in
return try get(with: key, options: options)
}
Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) -> String? in
return try get(with: key, options: options)
}
AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
return try set(value: value, with: key, options: options)
}
Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Bool in
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
return try set(value: value, with: key, options: options)
}
AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in
let noAuthSearchDictionary = query(with: key, options: options, requireAuthentication: false)
let authSearchDictionary = query(with: key, options: options, requireAuthentication: true)
let legacySearchDictionary = query(with: key, options: options)
SecItemDelete(legacySearchDictionary as CFDictionary)
SecItemDelete(authSearchDictionary as CFDictionary)
SecItemDelete(noAuthSearchDictionary as CFDictionary)
}
Function("canUseBiometricAuthentication") {() -> Bool in
let context = LAContext()
var error: NSError?
let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
if error != nil {
return false
}
return isBiometricsSupported
}
}
private func get(with key: String, options: SecureStoreOptions) throws -> String? {
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
if let unauthenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: false) {
return String(data: unauthenticatedItem, encoding: .utf8)
}
if let authenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: true) {
return String(data: authenticatedItem, encoding: .utf8)
}
if let legacyItem = try searchKeyChain(with: key, options: options) {
return String(data: legacyItem, encoding: .utf8)
}
return nil
}
private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
let valueData = value.data(using: .utf8)
setItemQuery[kSecValueData as String] = valueData
let accessibility = attributeWith(options: options)
if !options.requireAuthentication {
setItemQuery[kSecAttrAccessible as String] = accessibility
} else {
guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else {
throw MissingPlistKeyException()
}
let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil)
setItemQuery[kSecAttrAccessControl as String] = accessOptions
}
let status = SecItemAdd(setItemQuery as CFDictionary, nil)
switch status {
case errSecSuccess:
// On success we want to remove the other key alias and legacy key (if they exist) to avoid conflicts during reads
SecItemDelete(query(with: key, options: options) as CFDictionary)
SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary)
return true
case errSecDuplicateItem:
return try update(value: value, with: key, options: options)
default:
throw KeyChainException(status)
}
}
private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
var query = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
let valueData = value.data(using: .utf8)
let updateDictionary = [kSecValueData as String: valueData]
if let authPrompt = options.authenticationPrompt {
query[kSecUseOperationPrompt as String] = authPrompt
}
let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary)
if status == errSecSuccess {
return true
} else {
throw KeyChainException(status)
}
}
private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? {
var query = query(with: key, options: options, requireAuthentication: requireAuthentication)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = kCFBooleanTrue
if let authPrompt = options.authenticationPrompt {
query[kSecUseOperationPrompt as String] = authPrompt
}
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
switch status {
case errSecSuccess:
guard let item = item as? Data else {
return nil
}
return item
case errSecItemNotFound:
return nil
default:
throw KeyChainException(status)
}
}
private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) -> [String: Any] {
var service = options.keychainService ?? "app"
if let requireAuthentication {
service.append(":\(requireAuthentication ? "auth" : "no-auth")")
}
let encodedKey = Data(key.utf8)
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrGeneric as String: encodedKey,
kSecAttrAccount as String: encodedKey
]
}
private func attributeWith(options: SecureStoreOptions) -> CFString {
switch options.keychainAccessible {
case .afterFirstUnlock:
return kSecAttrAccessibleAfterFirstUnlock
case .afterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
case .always:
return kSecAttrAccessibleAlways
case .whenPasscodeSetThisDeviceOnly:
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
case .whenUnlocked:
return kSecAttrAccessibleWhenUnlocked
case .alwaysThisDeviceOnly:
return kSecAttrAccessibleAlwaysThisDeviceOnly
case .whenUnlockedThisDeviceOnly:
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
default:
return kSecAttrAccessibleWhenUnlocked
}
}
private func validate(for key: String) -> String? {
let trimmedKey = key.trimmingCharacters(in: .whitespaces)
if trimmedKey.isEmpty {
return nil
}
return key
}
}

View File

@@ -0,0 +1,15 @@
import ExpoModulesCore
internal struct SecureStoreOptions: Record {
@Field
var authenticationPrompt: String?
@Field
var keychainAccessible: SecureStoreAccessible = .whenUnlocked
@Field
var keychainService: String?
@Field
var requireAuthentication: Bool
}