Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions awcms-mobile/primary/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

<!-- Camera & Gallery permissions -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:label="awcms_mobile"
android:name="${applicationName}"
Expand Down
6 changes: 6 additions & 0 deletions awcms-mobile/primary/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,11 @@
<string>Untuk verifikasi lokasi Anda saat menggunakan aplikasi</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Untuk verifikasi lokasi Anda</string>

<!-- Camera & Gallery permissions -->
<key>NSCameraUsageDescription</key>
<string>Aplikasi memerlukan akses kamera untuk mengambil foto profil.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Aplikasi memerlukan akses galeri untuk memilih foto profil.</string>
</dict>
</plist>
10 changes: 5 additions & 5 deletions awcms-mobile/primary/lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// AWCMS Mobile - App Constants
///
///
/// Konstanta global yang digunakan di seluruh aplikasi.
library;

Expand All @@ -13,19 +13,19 @@ class AppConstants {

// API Endpoints
static const int apiTimeout = 30000; // 30 seconds

// Pagination
static const int defaultPageSize = 20;

// Cache Duration
static const Duration cacheDuration = Duration(hours: 1);

// Storage Keys
static const String tokenKey = 'auth_token';
static const String userKey = 'user_data';
static const String tenantKey = 'tenant_id';
static const String lastSyncKey = 'last_sync';

// Content Status
static const String statusDraft = 'draft';
static const String statusPublished = 'published';
Expand Down
59 changes: 59 additions & 0 deletions awcms-mobile/primary/lib/core/models/user_profile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// AWCMS Mobile - User Profile Model
///
/// Model untuk data profil pengguna yang diambil dari tabel 'users'.
library;

class UserProfile {
final String id;
final String? fullName;
final String? avatarUrl;
final String? email;
final String? role;

const UserProfile({
required this.id,
this.fullName,
this.avatarUrl,
this.email,
this.role,
});

factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
id: json['id'] as String,
fullName: json['full_name'] as String?,
avatarUrl: json['avatar_url'] as String?,
email: json['email'] as String?,
// Note: Role fetching strategy might need adjustment depending on
// whether we join with a roles table or just read a field.
// Based on React app: role comes from a joined 'roles' table or 'role' field.
// We'll keep it simple for now and map what we can.
role: json['role'] as String?,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'full_name': fullName,
'avatar_url': avatarUrl,
'email': email,
'role': role,
};
}

UserProfile copyWith({
String? fullName,
String? avatarUrl,
String? email,
String? role,
}) {
return UserProfile(
id: id,
fullName: fullName ?? this.fullName,
avatarUrl: avatarUrl ?? this.avatarUrl,
email: email ?? this.email,
role: role ?? this.role,
);
}
}
43 changes: 43 additions & 0 deletions awcms-mobile/primary/lib/core/providers/locale_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// AWCMS Mobile - Locale Provider
///
/// Provider untuk mengelola bahasa aplikasi (English/Indonesia).
library;

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../services/secure_storage_service.dart';

class LocaleNotifier extends Notifier<Locale> {
static const _localeKey = 'app_locale';

@override
Locale build() {
// Load saved locale or default to system/en
_loadSavedLocale();
return const Locale('en'); // Default initial
}

Future<void> _loadSavedLocale() async {
final savedCode = await SecureStorageService.instance.read(_localeKey);
if (savedCode != null) {
state = Locale(savedCode);
}
}

Future<void> setLocale(Locale locale) async {
state = locale;
await SecureStorageService.instance.write(_localeKey, locale.languageCode);
}

Future<void> toggleLocale() async {
final newLocale = state.languageCode == 'en'
? const Locale('id')
: const Locale('en');
await setLocale(newLocale);
}
}

final localeProvider = NotifierProvider<LocaleNotifier, Locale>(() {
return LocaleNotifier();
});
42 changes: 42 additions & 0 deletions awcms-mobile/primary/lib/core/providers/theme_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/// AWCMS Mobile - Theme Provider
///
/// Provider untuk mengelola mode tema aplikasi (Light/Dark/System).
library;

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../services/secure_storage_service.dart';

class ThemeNotifier extends Notifier<ThemeMode> {
static const _themeKey = 'app_theme_mode';

@override
ThemeMode build() {
_loadSavedTheme();
return ThemeMode.system; // Default
}

Future<void> _loadSavedTheme() async {
final savedMode = await SecureStorageService.instance.read(_themeKey);
if (savedMode != null) {
state = _parseThemeMode(savedMode);
}
}

Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
await SecureStorageService.instance.write(_themeKey, mode.name);
}

ThemeMode _parseThemeMode(String modeName) {
return ThemeMode.values.firstWhere(
(e) => e.name == modeName,
orElse: () => ThemeMode.system,
);
}
}

final themeProvider = NotifierProvider<ThemeNotifier, ThemeMode>(() {
return ThemeNotifier();
});
14 changes: 13 additions & 1 deletion awcms-mobile/primary/lib/core/services/auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import 'secure_storage_service.dart';
import 'security_service.dart';

import 'dart:developer' as developer;

/// Authentication state
enum AuthStatus { initial, authenticated, unauthenticated, loading, blocked }

Expand Down Expand Up @@ -128,16 +130,24 @@ class AuthService extends Notifier<AuthState> {

/// Sign in with email and password
Future<void> signInWithEmail(String email, String password) async {
developer.log('[AuthService] signInWithEmail called for $email');
// Check device security first
if (!await _checkDeviceSecurity()) return;
if (!await _checkDeviceSecurity()) {
developer.log('[AuthService] Device security check failed');
return;
}

state = state.copyWith(status: AuthStatus.loading, errorMessage: null);

try {
developer.log('[AuthService] Calling Supabase signInWithPassword...');
final response = await Supabase.instance.client.auth.signInWithPassword(
email: email,
password: password,
);
developer.log(
'[AuthService] Supabase response received. User: ${response.user?.id}',
);

if (response.user != null) {
state = AuthState(
Expand All @@ -151,11 +161,13 @@ class AuthService extends Notifier<AuthState> {
);
}
} on AuthException catch (e) {
developer.log('[AuthService] AuthException: ${e.message}');
state = AuthState(
status: AuthStatus.unauthenticated,
errorMessage: e.message,
);
} catch (e) {
developer.log('[AuthService] Unexpected error: $e');
state = const AuthState(
status: AuthStatus.unauthenticated,
errorMessage: 'An unexpected error occurred',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// AWCMS Mobile - Device Permission Service
///
/// Service untuk menangani permintaan izin perangkat (Kamera, Galeri).
library;

import 'dart:io';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';

class DevicePermissionService {
/// Request camera permission
Future<bool> requestCameraPermission() async {
final status = await Permission.camera.request();
return status.isGranted;
}

/// Request photos/gallery permission
Future<bool> requestPhotosPermission() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
// Android 13+ (API 33+) uses READ_MEDIA_IMAGES
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.photos.request();
return status.isGranted;
} else {
// Android < 13 uses READ_EXTERNAL_STORAGE
final status = await Permission.storage.request();
return status.isGranted;
}
} else {
// iOS
final status = await Permission.photos.request();
return status.isGranted;
}
}

/// Check current camera permission status
Future<bool> checkCameraPermission() async {
return await Permission.camera.isGranted;
}

/// Check current photos permission status
Future<bool> checkPhotosPermission() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
return await Permission.photos.isGranted;
} else {
return await Permission.storage.isGranted;
}
} else {
return await Permission.photos.isGranted;
}
}

/// Open app settings
Future<void> openSettings() async {
await openAppSettings();
}
}
Loading
Loading