Flutter firebase service
By: Nhan Cao
Tags: Flutter, Firebase

Firebase Analytics

Firebase Crashlytics

Firebase auth: Google, Facebook, Apple

Firebase storage: select image + upload, gen URL download file

Cloud functions

Firebase messaging + local push notification

Integrate firebase

https://medium.com/@nhancv/flutter-create-a-firebase-project-183b681e4cb5

Integrate firebase Analytics

https://medium.com/@nhancv/flutter-analytics-integration-768ae76a8077

Integrate firebase Crashlytics

https://medium.com/@nhancv/flutter-crashlytics-integration-17530e24ba5c

pubspec.yaml

# Firebase

firebase_core: ^0.5.2+1

firebase_analytics: ^6.2.0

cloud_firestore: ^0.14.3+1

firebase_storage: ^5.1.0

image_picker: ^0.6.7+14

cloud_functions: ^0.7.1

firebase_crashlytics: ^0.2.3+1

firebase_messaging: ^8.0.0-dev.9

flutter_local_notifications: ^3.0.1+6

firebase_auth: ^0.18.3+1

google_sign_in: ^4.5.6

apple_sign_in: ^0.1.0

flutter_facebook_login: ^3.0.0

android/build.gradle

buildscript {

ext.kotlin_version = '1.3.50'

repositories {

google()

jcenter()

}



dependencies {

classpath 'com.android.tools.build:gradle:3.5.0'

classpath 'com.google.gms:google-services:4.3.4'

classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

}

}



allprojects {

repositories {

google()

jcenter()

}

}



rootProject.buildDir = '../build'

subprojects {

project.buildDir = "${rootProject.buildDir}/${project.name}"

}

subprojects {

project.evaluationDependsOn(':app')

}



task clean(type: Delete) {

delete rootProject.buildDir

}

android/app/build.gradle

...

dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

implementation 'com.google.firebase:firebase-analytics:17.5.0'

implementation 'com.google.firebase:firebase-crashlytics:17.2.2'

} apply plugin: 'com.google.firebase.crashlytics'

apply plugin: 'com.google.gms.google-services'

android/app/src/main/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

<string name="app_name">AppName</string>



<!-- Replace "000000000000" with your Facebook App ID here. -->

<string name="facebook_app_id">000000000000</string>



<!--

Replace "000000000000" with your Facebook App ID here.

**NOTE**: The scheme needs to start with `fb` and then your ID.

-->

<string name="fb_login_protocol_scheme">fb000000000000</string>

</resources>

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.app.nft">



<uses-permission android:name="android.permission.INTERNET"/>



<!-- io.flutter.app.FlutterApplication is an android.app.Application that

calls FlutterMain.startInitialization(this); in its onCreate method.

In most cases you can leave this as-is, but you if you want to provide

additional functionality it is fine to subclass or reimplement

FlutterApplication and put your custom class here. -->

<application

android:label="AppName"

android:requestLegacyExternalStorage="true"

android:usesCleartextTraffic="true"

android:icon="@mipmap/ic_launcher">

<activity

android:name=".MainActivity"

android:launchMode="singleTop"

android:theme="@style/LaunchTheme"

android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

android:hardwareAccelerated="true"

android:windowSoftInputMode="adjustResize"

android:showWhenLocked="true"

android:turnScreenOn="true">

<!-- Specifies an Android theme to apply to this Activity as soon as

the Android process has started. This theme is visible to the user

while the Flutter UI initializes. After that, this theme continues

to determine the Window background behind the Flutter UI. -->

<meta-data

android:name="io.flutter.embedding.android.NormalTheme"

android:resource="@style/NormalTheme"

/>

<!-- Displays an Android View that continues showing the launch screen

Drawable until Flutter paints its first frame, then this splash

screen fades out. A splash screen is useful to avoid any visual

gap between the end of Android's launch screen and the painting of

Flutter's first frame. -->

<meta-data

android:name="io.flutter.embedding.android.SplashScreenDrawable"

android:resource="@drawable/launch_background"

/>

<intent-filter>

<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>

</intent-filter>

</activity>

<!-- Don't delete the meta-data below.

This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

<meta-data

android:name="flutterEmbedding"

android:value="2" />



<!-- A custom Android Notification Channel to deliver FCM notifications on a non-default channel -->

<meta-data

android:name="com.google.firebase.messaging.default_notification_channel_id"

android:value="high_importance_channel" />



<!-- Facebook config https://pub.dev/packages/flutter_facebook_login -->

<meta-data android:name="com.facebook.sdk.ApplicationId"

android:value="@string/facebook_app_id"/>



<activity android:name="com.facebook.FacebookActivity"

android:configChanges=

"keyboard|keyboardHidden|screenLayout|screenSize|orientation"

android:label="@string/app_name" />



<activity

android:name="com.facebook.CustomTabActivity"

android:exported="true">

<intent-filter>

<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="@string/fb_login_protocol_scheme" />

</intent-filter>

</activity>



</application>

</manifest>

android/app/proguard-rules.pro

#https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/proguard-rules.pro

## Flutter wrapper

-keep class io.flutter.app.** { *; }

-keep class io.flutter.plugin.** { *; }

-keep class io.flutter.util.** { *; }

-keep class io.flutter.view.** { *; }

-keep class io.flutter.** { *; }

-keep class io.flutter.plugins.** { *; }

-dontwarn io.flutter.embedding.**



## Gson rules

# Gson uses generic type information stored in a class file when working with fields. Proguard

# removes such information by default, so configure it to keep all of it.

-keepattributes Signature



# For using GSON @Expose annotation

-keepattributes *Annotation*



# Gson specific classes

-dontwarn sun.misc.**

#-keep class com.google.gson.stream.** { *; }



# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,

# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)

-keep class * extends com.google.gson.TypeAdapter

-keep class * implements com.google.gson.TypeAdapterFactory

-keep class * implements com.google.gson.JsonSerializer

-keep class * implements com.google.gson.JsonDeserializer



# Prevent R8 from leaving Data object members always null

-keepclassmembers,allowobfuscation class * {

@com.google.gson.annotations.SerializedName <fields>;

}

ios/Runner/Runner.entitlements

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>aps-environment</key>

<string>development</string>

<key>com.apple.developer.applesignin</key>

<array>

<string>Default</string>

</array>

</dict>

</plist>

Info.plist

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>CFBundleDevelopmentRegion</key>

<string>$(DEVELOPMENT_LANGUAGE)</string>

<key>CFBundleExecutable</key>

<string>$(EXECUTABLE_NAME)</string>

<key>CFBundleIdentifier</key>

<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

<key>CFBundleInfoDictionaryVersion</key>

<string>6.0</string>

<key>CFBundleName</key>

<string>AppName</string>

<key>CFBundlePackageType</key>

<string>APPL</string>

<key>CFBundleShortVersionString</key>

<string>$(FLUTTER_BUILD_NAME)</string>

<key>CFBundleSignature</key>

<string>????</string>

<key>CFBundleVersion</key>

<string>$(FLUTTER_BUILD_NUMBER)</string>

<key>ITSAppUsesNonExemptEncryption</key>

<false/>

<key>LSRequiresIPhoneOS</key>

<true/>

<key>NSAppTransportSecurity</key>

<dict>

<key>NSAllowsLocalNetworking</key>

<true/>

</dict>

<key>NSCameraUsageDescription</key>

<string>App requires access to the camera for avatar upload</string>

<key>NSPhotoLibraryUsageDescription</key>

<string>App requires access to the photo library for avatar upload</string>

<key>UIBackgroundModes</key>

<array>

<string>fetch</string>

<string>remote-notification</string>

</array>

<key>UILaunchStoryboardName</key>

<string>LaunchScreen</string>

<key>UIMainStoryboardFile</key>

<string>Main</string>

<key>UISupportedInterfaceOrientations</key>

<array>

<string>UIInterfaceOrientationPortrait</string>

</array>

<key>UISupportedInterfaceOrientations~ipad</key>

<array>

<string>UIInterfaceOrientationPortrait</string>

<string>UIInterfaceOrientationPortraitUpsideDown</string>

<string>UIInterfaceOrientationLandscapeLeft</string>

<string>UIInterfaceOrientationLandscapeRight</string>

</array>

<key>UIViewControllerBasedStatusBarAppearance</key>

<false/>

<!-- Put me in the [my_project]/ios/Runner/Info.plist file -->

<!-- Google Sign-in Section -->

<key>CFBundleURLTypes</key>

<array>

<dict>

<key>CFBundleTypeRole</key>

<string>Editor</string>

<key>CFBundleURLSchemes</key>

<array>

<!-- TODO Replace this value: -->

<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->

<string>com.googleusercontent.apps.1022986619993-tgkhigi1onm47tbp2iic1cnbuun9l788</string>

</array>

</dict>

<dict>

<key>CFBundleURLSchemes</key>

<array>

<!--

Replace "000000000000" with your Facebook App ID here.

**NOTE**: The scheme needs to start with `fb` and then your ID.

-->

<string>fb000000000000</string>

</array>

</dict>

</array>



<key>FacebookAppID</key>



<!-- Replace "000000000000" with your Facebook App ID here. -->

<string>000000000000</string>

<key>FacebookDisplayName</key>



<!-- Replace "YOUR_APP_NAME" with your Facebook App name. -->

<string>YOUR_APP_NAME</string>



<key>LSApplicationQueriesSchemes</key>

<array>

<string>fbapi</string>

<string>fb-messenger-share-api</string>

<string>fbauth2</string>

<string>fbshareextension</string>

</array>

<!-- End of the Google and Fb Sign-in Section -->

</dict>

</plist>

AppDelegate.swift

import UIKit

import Flutter



@UIApplicationMain

@objc class AppDelegate: FlutterAppDelegate {

override func application(

_ application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?

) -> Bool {

if #available(iOS 10.0, *) {

UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate

}

if(!UserDefaults.standard.bool(forKey: "Notification")) {

UIApplication.shared.cancelAllLocalNotifications()

UserDefaults.standard.set(true, forKey: "Notification")

}

GeneratedPluginRegistrant.register(with: self)

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

}

}

my_app.dart

import 'dart:async';

import 'dart:isolate';



import 'package:firebase_crashlytics/firebase_crashlytics.dart';

import 'package:firebase_analytics/observer.dart';

import 'package:firebase_core/firebase_core.dart';

import 'package:flutter/foundation.dart';

import 'package:flutter/material.dart';

import 'package:flutter/services.dart';

import 'package:flutter_localizations/flutter_localizations.dart';

import 'package:nft/generated/l10n.dart';

import 'package:nft/pages/home/home_provider.dart';

import 'package:nft/pages/login/login_provider.dart';

import 'package:nft/pages/play/play_provider.dart';

import 'package:nft/services/app/app_dialog.dart';

import 'package:nft/services/app/app_loading.dart';

import 'package:nft/services/cache/credential.dart';

import 'package:nft/services/cache/storage.dart';

import 'package:nft/services/cache/storage_preferences.dart';

import 'package:nft/services/app/locale_provider.dart';

import 'package:nft/services/firebase/firebase_push.dart';

import 'package:nft/services/firebase/firebase_tracking.dart';

import 'package:nft/services/firebase/local_push.dart';

import 'package:nft/services/rest_api/api_user.dart';

import 'package:nft/utils/app_constant.dart';

import 'package:nft/utils/app_route.dart';

import 'package:nft/utils/app_theme.dart';

import 'package:provider/provider.dart';

import 'package:provider/single_child_widget.dart';



Future<void> myMain() async {

/// Start services later

WidgetsFlutterBinding.ensureInitialized();



/// Force portrait mode

await SystemChrome.setPreferredOrientations(

<DeviceOrientation>[DeviceOrientation.portraitUp]);



// Initializing FlutterFire

await Firebase.initializeApp();



// Initialize Crash report

const bool enableCrashlytics = !kDebugMode;

await FirebaseCrashlytics.instance

.setCrashlyticsCollectionEnabled(enableCrashlytics);



// Pass all uncaught errors from the framework to Crashlytics.

FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;



// Errors outside of Flutter

Isolate.current.addErrorListener(RawReceivePort((List<dynamic> pair) async {

final List<dynamic> errorAndStacktrace = pair;

await FirebaseCrashlytics.instance.recordError(

errorAndStacktrace.first,

errorAndStacktrace.last as StackTrace,

);

}).sendPort);



// Config firebase message

await FirebasePush.I.configBeforeRunApp();



/// Run Application

// Zoned Errors

runZonedGuarded<Future<void>>(() async {

/// Run Application

runApp(

MultiProvider(

providers: <SingleChildWidget>[

Provider<AppRoute>(create: (_) => AppRoute()),

Provider<Storage>(create: (_) => StoragePreferences()),

ChangeNotifierProvider<Credential>(

create: (BuildContext context) =>

Credential(context.read<Storage>())),

ProxyProvider<Credential, ApiUser>(

create: (_) => ApiUser(),

update: (_, Credential credential, ApiUser userApi) {

return userApi..token = credential.token;

}),

Provider<AppLoadingProvider>(create: (_) => AppLoadingProvider()),

Provider<AppDialogProvider>(create: (_) => AppDialogProvider()),

ChangeNotifierProvider<LocaleProvider>(

create: (_) => LocaleProvider()),

ChangeNotifierProvider<AppThemeProvider>(

create: (_) => AppThemeProvider()),

ChangeNotifierProvider<HomeProvider>(

create: (BuildContext context) => HomeProvider(

context.read<ApiUser>(),

context.read<Credential>(),

)),

ChangeNotifierProvider<PlayProvider>(

create: (BuildContext context) => PlayProvider()),

ChangeNotifierProvider<LoginProvider>(

create: (BuildContext context) => LoginProvider(

context.read<ApiUser>(),

context.read<Credential>(),

)),

],

child: const MyApp(),

),

);

}, FirebaseCrashlytics.instance.recordError);

}



class MyApp extends StatefulWidget {

const MyApp({Key key}) : super(key: key);



@override

_MyAppState createState() => _MyAppState();

}



class _MyAppState extends State<MyApp> {

static FirebaseAnalyticsObserver observer =

FirebaseAnalyticsObserver(analytics: FirebaseTracking.I.analytics);



@override

void initState() {

super.initState();



// Init firebase push

FirebasePush.I.configInAppState();

// Init local push

LocalPush.I.configInAppState(

onSelectNotification: (String payload) async {

print('onSelectNotification $payload');

},

onDidReceiveLocalNotification:

(int id, String title, String body, String payload) async {

print('onDidReceiveLocalNotification $payload');

},

);



// Example about load credential to init page

WidgetsBinding.instance.addPostFrameCallback((_) async {

final bool hasCredential =

await context.read<Credential>().loadCredential();

if (hasCredential) {

context.navigator()?.pushReplacementNamed(AppConstant.homePageRoute);

}

});

}



@override

Widget build(BuildContext context) {

// Get providers

final AppRoute appRoute = context.watch<AppRoute>();

final LocaleProvider localeProvider = context.watch<LocaleProvider>();

final AppTheme appTheme = context.theme();

// Build Material app

return MaterialApp(

navigatorKey: appRoute.navigatorKey,

locale: localeProvider.locale,

supportedLocales: S.delegate.supportedLocales,

localizationsDelegates: const <LocalizationsDelegate<dynamic>>[

S.delegate,

GlobalMaterialLocalizations.delegate,

GlobalCupertinoLocalizations.delegate,

GlobalWidgetsLocalizations.delegate,

],

debugShowCheckedModeBanner: false,

theme: appTheme.buildThemeData(),

//https://stackoverflow.com/questions/57245175/flutter-dynamic-initial-route

//https://github.com/flutter/flutter/issues/12454

//home: (appRoute.generateRoute(

/// const RouteSettings(name: AppConstant.rootPageRoute))

/// as MaterialPageRoute<dynamic>)

/// .builder(context),

initialRoute: AppConstant.demoPageRoute,

// initialRoute: AppConstant.rootPageRoute,

onGenerateRoute: appRoute.generateRoute,

navigatorObservers: <NavigatorObserver>[appRoute.routeObserver, observer],

);

}



}

demo_page.dart

import 'dart:async';

import 'dart:io';



import 'package:cloud_firestore/cloud_firestore.dart';

import 'package:firebase_crashlytics/firebase_crashlytics.dart';

import 'package:flutter/cupertino.dart';

import 'package:flutter/material.dart';

import 'package:habitapp/services/app/app_loading.dart';

import 'package:habitapp/services/app/dynamic_size.dart';

import 'package:habitapp/services/firebase/cloud_firestore.dart';

import 'package:habitapp/services/firebase/cloud_functions.dart';

import 'package:habitapp/services/firebase/cloud_storage.dart';

import 'package:habitapp/services/firebase/firebase_error.dart';

import 'package:habitapp/services/firebase/firebase_error_type.dart';

import 'package:habitapp/services/firebase/firebase_login.dart';

import 'package:habitapp/services/firebase/local_push.dart';

import 'package:habitapp/utils/app_helper.dart';

import 'package:habitapp/utils/app_log.dart';

import 'package:habitapp/utils/app_style.dart';

import 'package:habitapp/widgets/p_appbar_empty.dart';

import 'package:habitapp/widgets/w_dismiss_keyboard.dart';

import 'package:image_picker/image_picker.dart';

import 'package:provider/provider.dart';



class DemoPage extends StatefulWidget {

@override

_DemoPageState createState() => _DemoPageState();

}



class _DemoPageState extends State<DemoPage>

with WidgetsBindingObserver, DynamicSize, FirebaseError {

CloudFireStoreSnapshot _cloudFireStoreSnapshot;

StreamSubscription<DocumentSnapshot> snapshotSubscription;



@override

void initState() {

super.initState();

print('page1_initState');

WidgetsBinding.instance.addObserver(this);

WidgetsBinding.instance.addPostFrameCallback((_) {

if (_cloudFireStoreSnapshot != null) {

snapshotSubscription =

CloudFireStore.I.listenUser(_cloudFireStoreSnapshot);

}

});

}



@override

void didChangeDependencies() {

super.didChangeDependencies();

print('page1_didChangeDependencies');

}



@override

void didUpdateWidget(covariant DemoPage oldWidget) {

super.didUpdateWidget(oldWidget);

print('page1_didUpdateWidget');

}



@override

void deactivate() {

print('page1_deactivate');

super.deactivate();

}



@override

void dispose() {

print('page1_dispose');

snapshotSubscription?.cancel();

WidgetsBinding.instance.removeObserver(this);

super.dispose();

}



@override

void didChangeAppLifecycleState(AppLifecycleState state) {

super.didChangeAppLifecycleState(state);

print('page1_didChangeAppLifecycleState: $state');

}



@override

Widget build(BuildContext context) {

initDynamicSize(context);

return PAppBarEmpty(

child: WDismissKeyboard(

child: Container(

width: double.infinity,

child: Column(

mainAxisAlignment: MainAxisAlignment.center,

crossAxisAlignment: CrossAxisAlignment.center,

mainAxisSize: MainAxisSize.max,

children: <Widget>[

FlatButton(

onPressed: () {

FirebaseLogin.I.signInGoogle();

},

child: const Text(

'Google Sign in',

),

),

FlatButton(

onPressed: () {

FirebaseLogin.I.signInFacebook();

},

child: const Text(

'Facebook Sign in',

),

),

FutureBuilder<bool>(

future: FirebaseLogin.I.isAppleSignInAvailable(),

builder: (_, AsyncSnapshot<bool> isAvailable) {

return isAvailable.data == true

? FlatButton(

onPressed: () {

FirebaseLogin.I.signInApple();

},

child: const Text(

'Facebook Apple',

),

)

: Container();

},

),

FlatButton(

onPressed: () {

LocalPush.I.schedule(

id: 0,

message: 'local push',

notifyAt:

DateTime.now().add(const Duration(seconds: 3)),

payload: NotyPayload(0, data: 'default'));

},

child: const Text(

'Local push',

),

),

FlatButton(

onPressed: () {

firebaseCallSafety(

CloudFunctions.I.getHelloWorld,

onStart: () async {

print('Start call function');

},

onCompleted: (bool status, void _) async {

print('Call function status: $status');

},

onError: (dynamic error) async {

print(error);

},

);

},

child: const Text(

'Hello world',

),

),

FlatButton(

onPressed: () {

// Forcing a crash

FirebaseCrashlytics.instance.crash();

},

child: const Text('Crash'),

),

FlatButton(

onPressed: () {

firebaseCallSafety(CloudFireStore.I.addUserWithId,

onCompleted: (bool status, void _) async {

print('User added with status: $status');

});

},

child: const Text(

'Add User',

),

),

FlatButton(

onPressed: () {

firebaseCallSafety(CloudFireStore.I.updateUser,

onCompleted: (bool status, void _) async {

print('User updated with status: $status');

});

},

child: const Text(

'Update User',

),

),

FlatButton(

onPressed: () {

firebaseCallSafety(CloudFireStore.I.removeExtraField,

onCompleted: (bool status, void _) async {

print('User remove extra field with status: $status');

});

},

child: const Text(

'Remove extra field',

),

),

FlatButton(

onPressed: () {

firebaseCallSafety(CloudFireStore.I.deleteUser,

onCompleted: (bool status, void _) async {

print('User deleted with status: $status');

});

},

child: const Text(

'Delete user',

),

),

FlatButton(

onPressed: () {

firebaseCallSafety(() => CloudFireStore.I.readUser(),

onCompleted: (bool status, String value) async {

print('User read with status: $status, value: $value');

if (status == true) {

AppHelper.showToast(value);

}

});

},

child: const Text(

'Read user info',

),

),

ChangeNotifierProvider<CloudFireStoreSnapshot>(

create: (_) => CloudFireStoreSnapshot(),

builder: (BuildContext context, _) {

_cloudFireStoreSnapshot =

Provider.of<CloudFireStoreSnapshot>(context,

listen: false);

return Selector<CloudFireStoreSnapshot, DocumentSnapshot>(

selector: (_, CloudFireStoreSnapshot p) => p.userSnapshot,

builder: (_, DocumentSnapshot snapshot, __) {

print('update changes page1');

return Text(snapshot?.exists == true

? snapshot.data()['full_name'] as String

: 'null');

},

);

},

),

IconButton(

icon: const Icon(Icons.photo_library),

onPressed: () {

_selectImageSource(context);

},

),

],

)),

),

);

}



Future<void> _selectImageSource(BuildContext context) async {

// Close keyboard

FocusScope.of(context).requestFocus(FocusNode());



// Define max size of image

const double maxWidth = 350;

const double maxHeight = 500;



// Show choose image source

final List<String> source = <String>['Camera', 'Gallery'];

final List<CupertinoActionSheetAction> actions = source

.map((String it) => CupertinoActionSheetAction(

isDefaultAction: true,

onPressed: () {

// pop value

Navigator.pop<int>(context, source.indexOf(it));

},

child: Text(

it,

style: boldTextStyle(14, Colors.black),

),

))

.toList(growable: false);



final int index = await showCupertinoModalPopup<int>(

context: context,

builder: (BuildContext context) => CupertinoActionSheet(

actions: actions,

cancelButton: CupertinoActionSheetAction(

isDefaultAction: true,

isDestructiveAction: true,

onPressed: () {

// pop null

Navigator.pop(context);

},

child: Text(

'Close',

style: boldTextStyle(14, Colors.red),

),

),

),

);



if (index != null) {

final ImagePicker _imagePicker = ImagePicker();

final PickedFile image = await _imagePicker.getImage(

source: index == 0 ? ImageSource.camera : ImageSource.gallery,

maxWidth: maxWidth,

maxHeight: maxHeight,

);

if (image != null) {

final File file = File(image.path);



// Upload media image

firebaseCallSafety(() {

return CloudStorage.I.uploadFile(file);

}, onStart: () async {

AppLoadingProvider.show(context);

}, onCompleted: (bool status, void _) async {

AppLoadingProvider.hide(context);

if (status) {

AppHelper.showToast('success');

}

});

}

}

}



@override

Future<void> onFirebaseError(dynamic error) async {

final FirebaseErrorType firebaseErrorType = parseFirebaseErrorType(error);

print(firebaseErrorType);

AppHelper.showToast(firebaseErrorType.message);

}

}

firebase_tracking.dart

import 'package:firebase_analytics/firebase_analytics.dart';

import 'package:flutter/foundation.dart';



class FirebaseTracking {

FirebaseTracking._() {

disableLog = !kReleaseMode;

}



static FirebaseTracking I = FirebaseTracking._();



// Firebase analytics instance

final FirebaseAnalytics _analytics = FirebaseAnalytics();



FirebaseAnalytics get analytics => _analytics;



// When the debug slider is ON and no event data is sent

bool _disableLog = true;



set disableLog(bool value) {

_disableLog = value;

_analytics.setAnalyticsCollectionEnabled(!value);

}



/// Log button tapped event

void logEvent({String name, Map<String, dynamic> parameters}) {

if (_disableLog) {

return;

}

if (name == null || name.isEmpty) {

return;

}

_analytics.logEvent(name: name, parameters: parameters);

}



/// Tracking screen

void trackingScreen(String screenName) {

if (_disableLog) {

return;

}

_analytics.setCurrentScreen(screenName: screenName);

}

}

firebase_error.dart

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';

import 'package:nft/services/firebase/firebase_error_type.dart';

import 'package:nft/utils/app_log.dart';



mixin FirebaseError {

/// This function was called when trigger safeCallApi

/// and apiError = true as default

Future<void> onFirebaseError(dynamic error);



/// Call api safety with error handling.

/// Required:

/// - dioApi: call async dio function

/// Optional:

/// - onStart: the function executed before api, can be null

/// - onError: the function executed in case api crashed, can be null

/// - onCompleted: the function executed after api or before crashing, can be null

/// - onFinally: the function executed end of function, can be null

/// - apiError: true as default if you want to forward the error to onApiError

Future<T> firebaseCallSafety<T>(

Future<T> Function() fireCall, {

Future<void> Function() onStart,

Future<void> Function(dynamic error) onError,

Future<void> Function(bool status, T res) onCompleted,

Future<void> Function() onFinally,

bool skipOnError = true,

}) async {

try {

// On start, use for show loading

if (onStart != null) {

await onStart();

}



// Execute api

final T res = await fireCall();



// On completed, use for hide loading

if (onCompleted != null) {

await onCompleted(true, res);

}

// Return api response

return res;

} catch (error) {

// In case error:

// On completed, use for hide loading

if (onCompleted != null) {

await onCompleted(false, null);

}



// On inline error

if (onError != null) {

await onError(error);

}



// Call onApiError if apiError's enabled

if (skipOnError) {

onFirebaseError(error);

}



return null;

} finally {

// Call finally function

if (onFinally != null) {

await onFinally();

}

}

}



// Parsing error to ErrorType

FirebaseErrorType parseFirebaseErrorType(dynamic error) {

if (error is FirebaseException) {

FirebaseErrorCode errorCode = FirebaseErrorCode.unknown;

if(error.code == 'permission-denied') {

errorCode = FirebaseErrorCode.permission;

}

return FirebaseErrorType(code: errorCode, message: error.message);

} else {

logger.e(error);

}

return FirebaseErrorType();

}

}

firebase_error_type.dart

enum FirebaseErrorCode { unknown, permission }



class FirebaseErrorType {

FirebaseErrorType({this.code = FirebaseErrorCode.unknown, this.message = 'Unknown'});



final FirebaseErrorCode code;

final String message;



@override

String toString() {

return 'FirebaseErrorType{code: $code, message: $message}';

}

}

cloud_firestore.dart

import 'dart:async';



import 'package:cloud_firestore/cloud_firestore.dart';

import 'package:nft/services/safety/change_notifier_safety.dart';



const String defaultDocumentId = 'g5P2RamQ0z00XeL08Kk2';



// dev rule

//rules_version = '2';

// service cloud.firestore {

// match /databases/{database}/documents {

// match /{document=**} {

// allow read, write: if

// request.time < timestamp.date(2020, 12, 27);

// }

// }

// }

class CloudFireStore {

CloudFireStore._();



static final CloudFireStore I = CloudFireStore._();



final FirebaseFirestore _store = FirebaseFirestore.instance;



// Insert

Future<void> addUser() {

final CollectionReference users = _store.collection('users');

final Map<String, String> data = <String, String>{

'full_name': 'test',

'extra': 'empty'

};

return users.add(data);

}



// Insert with defined id

Future<void> addUserWithId({String documentId = defaultDocumentId}) {

final CollectionReference users = _store.collection('users');

final Map<String, String> data = <String, String>{

'full_name': 'test',

'extra': 'empty'

};

return users.doc(documentId).set(data);

}



// Update document

Future<void> updateUser({String documentId = defaultDocumentId}) {

final CollectionReference users = _store.collection('users');

final Map<String, String> data = <String, String>{

'full_name': DateTime.now().toIso8601String()

};

return users.doc(documentId).update(data);

}



// Remove specific properties

Future<void> removeExtraField({String documentId = defaultDocumentId}) {

final CollectionReference users = _store.collection('users');

return users

.doc(documentId)

.update(<String, FieldValue>{'extra': FieldValue.delete()});

}



// Delete document

Future<void> deleteUser({String documentId = defaultDocumentId}) {

final CollectionReference users = _store.collection('users');

return users.doc(documentId).delete();

}



// One-time Read

Future<String> readUser({String documentId = defaultDocumentId}) async {

final CollectionReference users = _store.collection('users');

final DocumentSnapshot snapshot = await users.doc(documentId).get();

if (!snapshot.exists) {

throw Exception('User does not exist!');

}

return snapshot.data()['full_name'] as String;

}



// Real-time Read

// Remember cancel stream in dispose widget

StreamSubscription<DocumentSnapshot> listenUser(

final CloudFireStoreSnapshot cloudFireStoreSnapshot,

{String documentId = defaultDocumentId}) {

print('cloudFireStoreSnapshot ${cloudFireStoreSnapshot.hashCode}');

final CollectionReference users = _store.collection('users');

final Stream<DocumentSnapshot> documentStream =

users.doc(documentId).snapshots();

final StreamSubscription<DocumentSnapshot> subscription =

documentStream.listen((DocumentSnapshot event) {

print('update_cloudFireStoreSnapshot ${cloudFireStoreSnapshot.hashCode}');

cloudFireStoreSnapshot?.userSnapshot = event;

});

return subscription;

}

}



class CloudFireStoreSnapshot extends ChangeNotifierSafety {

DocumentSnapshot _userSnapshot;



DocumentSnapshot get userSnapshot => _userSnapshot;



set userSnapshot(DocumentSnapshot value) {

_userSnapshot = value;

notifyListeners();

}

}

cloud_storage.dart

import 'dart:async';

import 'dart:io';

import 'package:nft/utils/app_log.dart';

import 'package:path/path.dart';

import 'package:firebase_storage/firebase_storage.dart';



// dev rule

//rules_version = '2';

// service firebase.storage {

// match /b/{bucket}/o {

// match /{allPaths=**} {

// allow read, write: if

// request.time < timestamp.date(2020, 12, 27);

// }

// }

// }

class CloudStorage {

CloudStorage._();



static final CloudStorage I = CloudStorage._();



final FirebaseStorage _storage = FirebaseStorage.instance;



// Parse file name

String _parseFilename(File file) {

final String extension = basename(file.path).split('.').last;

final int nowTimeStamp = DateTime.now().toUtc().millisecondsSinceEpoch;

return '$nowTimeStamp.$extension';

}



// List files

Future<void> listFiles() async {

final ListResult result = await FirebaseStorage.instance.ref().listAll();



// ignore: avoid_function_literals_in_foreach_calls

result.items.forEach((Reference ref) {

print('Found file: $ref');

});



// ignore: avoid_function_literals_in_foreach_calls

result.prefixes.forEach((Reference ref) {

print('Found directory: $ref');

});

}



// Upload file

Future<void> uploadFile(File file) async {

final String uploadFileName = _parseFilename(file);

final UploadTask uploadTask =

FirebaseStorage.instance.ref('uploads/$uploadFileName').putFile(file);

final StreamSubscription<TaskSnapshot> streamSubscription =

uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {

// paused, running, success

logger.d('Snapshot state: ${snapshot.state}');

logger.d('Progress: ${snapshot.totalBytes / snapshot.bytesTransferred}');

});

await uploadTask;

streamSubscription.cancel();

}



// Generate download file url

Future<String> getDownloadURL(String path) =>

_storage.ref(path).getDownloadURL().then((String url) => url);

}

cloud_functions.dart

import 'dart:async';

import 'dart:io';

import 'package:nft/utils/app_log.dart';

import 'package:cloud_functions/cloud_functions.dart';



// Test local with mobile emulator and firebase emulators

const bool EMULATOR = true;



class CloudFunctions {

CloudFunctions._();



static final CloudFunctions I = CloudFunctions._();



final FirebaseFunctions _functions = FirebaseFunctions.instance;



// Call default cloud function

Future<void> getHelloWorld() async {

logger.d('Call cloud function helloWorld');

if (EMULATOR) {

_functions.useFunctionsEmulator(origin: 'http://localhost:5001');

}

// Format return from server:

// response.send({data: "Hello from Firebase!", error: ""});

final HttpsCallable callable = _functions.httpsCallable('helloWorld');

final HttpsCallableResult<String> results = await callable();

final String hello = results.data;

logger.d('From server: $hello');

}

}

firebase_push.dart

import 'package:firebase_core/firebase_core.dart';

import 'package:firebase_messaging/firebase_messaging.dart';

import 'package:flutter_local_notifications/flutter_local_notifications.dart';



/// Define a top-level named handler which background/terminated messages will

/// call.

///

/// To verify things are working, check out the native platform logs.

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {

// If you're going to use other Firebase services in the background, such as Firestore,

// make sure you call `initializeApp` before using other Firebase services.

await Firebase.initializeApp();

print('Handling a background message ${message.messageId}');

}



/// Create a [AndroidNotificationChannel] for heads up notifications

const AndroidNotificationChannel _channel = AndroidNotificationChannel(

'high_importance_channel', // id

'High Importance Notifications', // title

'This channel is used for important notifications.', // description

importance: Importance.high,

enableVibration: true,

playSound: true,

);



/// Initialize the [FlutterLocalNotificationsPlugin] package.

final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =

FlutterLocalNotificationsPlugin();



class FirebasePush {

FirebasePush._();



static FirebasePush I = FirebasePush._();



// Firebase messaging instance

final FirebaseMessaging _messaging = FirebaseMessaging.instance;



// Config place before runApp()

Future<void> configBeforeRunApp() async {

// Set the background messaging handler early on, as a named top-level function

FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);



/// Config: https://firebase.flutter.dev/docs/messaging/notifications

/// iOS: Enabling foreground notifications is generally a straightforward process.

/// Update the iOS foreground notification presentation options to allow

/// heads up notifications.

await _messaging.setForegroundNotificationPresentationOptions(

alert: true, // Required to display a heads up notification

badge: true,

sound: true,

);



/// We use this channel in the `AndroidManifest.xml` file to override the

/// default FCM channel to enable heads up notifications.

await _flutterLocalNotificationsPlugin

.resolvePlatformSpecificImplementation<

AndroidFlutterLocalNotificationsPlugin>()

?.createNotificationChannel(_channel);

}



// Place this config in initState of Application Widget

Future<void> configInAppState() async {

// Request permission

final NotificationSettings settings = await _messaging.requestPermission(

alert: true,

announcement: false,

badge: true,

carPlay: false,

criticalAlert: false,

provisional: false,

sound: true,

);

if (settings.authorizationStatus == AuthorizationStatus.authorized) {

print('User granted permission');



////////////////////////////////////////////////////////////////////

// Get any messages which caused the application to open from

// a terminated state.

final RemoteMessage initialMessage =

await FirebaseMessaging.instance.getInitialMessage();

print('initialMessage: ${initialMessage?.notification?.title}');



// Also handle any interaction when the app is in the background via a

// Stream listener

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {

print('A new onMessageOpenedApp event was published!');

print('onMessageOpenedApp: ${message.notification.title}');

});



// Listen new message

FirebaseMessaging.onMessage.listen((RemoteMessage message) {

print('message: ${message.notification.title}');

final RemoteNotification notification = message.notification;

final AndroidNotification android = message.notification?.android;



if (notification != null && android != null) {

_flutterLocalNotificationsPlugin.show(

notification.hashCode,

notification.title,

notification.body,

NotificationDetails(

android: AndroidNotificationDetails(

_channel.id,

_channel.name,

_channel.description,

icon: 'ic_launcher_foreground',

importance: Importance.max,

priority: Priority.max,

),

));

}

});



final String fcmToken = await _messaging.getToken();

print('fcmToken: $fcmToken');

} else if (settings.authorizationStatus ==

AuthorizationStatus.provisional) {

print('User granted provisional permission');

} else {

print('User declined or has not accepted permission');

}

}

}

local_push.dart

import 'dart:convert';



import 'package:flutter/cupertino.dart';

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

import 'package:nft/utils/app_log.dart';

import 'package:timezone/data/latest.dart' as tz;

import 'package:timezone/timezone.dart' as tz;



// Payload for notification

class NotyPayload {

NotyPayload(this.type, {this.data});



factory NotyPayload.fromJson(Map<String, dynamic> json) {

return NotyPayload(json['type'] as int, data: json['data'] as String);

}



final int type;

final String data;



String toJson() => jsonEncode(<String, dynamic>{

'type': type,

'data': data,

});



@override

String toString() {

return 'NotyPayload{type: $type, data: $data}';

}

}



class LocalPush {

LocalPush._();



static LocalPush I = LocalPush._();



// Place this config in initState of Application Widget

Future<bool> configInAppState({

Future<void> Function(String payload) onSelectNotification,

Future<void> Function(int id, String title, String body, String payload)

onDidReceiveLocalNotification,

}) {

// Initialise the time zone database

tz.initializeTimeZones();



// Initialise setting

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =

FlutterLocalNotificationsPlugin();

const AndroidInitializationSettings initializationSettingsAndroid =

AndroidInitializationSettings('ic_launcher_foreground');

final IOSInitializationSettings initializationSettingsIOS =

IOSInitializationSettings(

onDidReceiveLocalNotification: onDidReceiveLocalNotification);

final InitializationSettings initializationSettings =

InitializationSettings(

android: initializationSettingsAndroid,

iOS: initializationSettingsIOS);

return flutterLocalNotificationsPlugin.initialize(initializationSettings,

onSelectNotification: onSelectNotification);

}



static const String DEFAULT_SOUND = 'default';



/// Unit generate notify id by string

/// Format: key@group@extra

/// Ex: stackId@reminder@2020/09/11

int genNewNotifyId(String key, String group,

{String extra = 'default'}) {

return '$key@$group@$extra'.hashCode.abs();

}



/// About notify id: using 32bit integer.

/// Note: can use hasCode of string to get int for id

/// Format: '<payloadId>@<category>_millisecondsSinceEpoch'.hashCode

/// Ex: 'stackId@reminder_millisecondsSinceEpoch'.hashCode

Future<void> schedule(

{@required int id,

String title,

@required String message,

@required DateTime notifyAt,

String sound = DEFAULT_SOUND,

NotyPayload payload,

int badgeNumber}) async {

logger.d(

'noti_schedule: id{$id} title{$title} message{$message} notifyAt{$notifyAt} sound{$sound}');

final FlutterLocalNotificationsPlugin notifications =

FlutterLocalNotificationsPlugin();

// sound based on channel

final AndroidNotificationDetails androidDetails =

AndroidNotificationDetails(

'primary_$sound',

'Channel ${sound ?? 'no sound'}',

'Notifications for ${sound ?? 'no sound'}',

importance: Importance.max,

priority: Priority.max,

playSound: sound != null,

sound: sound == DEFAULT_SOUND || sound == null

? null

: RawResourceAndroidNotificationSound(sound),

);

final IOSNotificationDetails iOSDetails = IOSNotificationDetails(

sound: sound == DEFAULT_SOUND || sound == null ? null : '$sound.aiff',

presentAlert: true,

presentBadge: true,

presentSound: sound != null,

badgeNumber: badgeNumber);

final NotificationDetails platformChannelSpecifics =

NotificationDetails(android: androidDetails, iOS: iOSDetails);



if (notifyAt != null) {

await notifications.zonedSchedule(

id,

title,

message,

tz.TZDateTime.from(notifyAt, tz.local),

platformChannelSpecifics,

androidAllowWhileIdle: true,

uiLocalNotificationDateInterpretation:

UILocalNotificationDateInterpretation.absoluteTime,

payload: payload?.toJson(),

);

}

}



/// Cancel notification by id

Future<void> cancel(int id) async {

return FlutterLocalNotificationsPlugin().cancel(id);

}



/// Cancel all push notification

Future<void> cancelAll() async {

return FlutterLocalNotificationsPlugin().cancelAll();

}

}

firebase_login.dart

import 'dart:async';

import 'package:apple_sign_in/apple_sign_in.dart';

import 'package:firebase_auth/firebase_auth.dart';

import 'package:flutter_facebook_login/flutter_facebook_login.dart';

import 'package:google_sign_in/google_sign_in.dart';

import 'package:nft/utils/app_log.dart';



class AuthCanceled implements Exception {

@override

String toString() => 'AuthCanceled: cancelled by user';

}



class UserNotAuthorized implements Exception {

@override

String toString() => 'User not authorized!';

}



class FirebaseLogin {

FirebaseLogin._();



static final FirebaseLogin I = FirebaseLogin._();



final FirebaseAuth _auth = FirebaseAuth.instance;

final GoogleSignIn _googleSignIn = GoogleSignIn();

final FacebookLogin _facebookLogin = FacebookLogin();



Future<bool> isAppleSignInAvailable() => AppleSignIn.isAvailable();



Future<User> signInGoogle() async {

final GoogleSignInAccount googleUser = await _googleSignIn.signIn();

if (googleUser == null) {

return null;

}

final GoogleSignInAuthentication googleAuth =

await googleUser?.authentication;



final AuthCredential credential = GoogleAuthProvider.credential(

accessToken: googleAuth.accessToken,

idToken: googleAuth.idToken,

);

final User user = (await _auth.signInWithCredential(credential)).user;

return user;

}



Future<User> signInApple() async {

final AuthorizationResult loginResult = await AppleSignIn.performRequests([

const AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])

]);

switch (loginResult.status) {

case AuthorizationStatus.authorized:

// here we're going to sign in the user within firebase

break;

case AuthorizationStatus.error:

// do something

throw loginResult.error;



case AuthorizationStatus.cancelled:

throw AuthCanceled();

}



final AppleIdCredential appleIdCredential = loginResult.credential;



final AuthCredential credential = OAuthProvider('apple.com').credential(

accessToken: String.fromCharCodes(appleIdCredential.authorizationCode),

idToken: String.fromCharCodes(appleIdCredential.identityToken),

);



final UserCredential result = await _auth.signInWithCredential(credential);

final User user = result.user;

logger.d('Signed in with apple ${user.uid}');

return user;

}





Future<User> signInFacebook() async {

final FacebookLoginResult result =

await _facebookLogin.logIn(<String>['email']);

final AuthCredential facebookAuthCred =

FacebookAuthProvider.credential(result.accessToken.token);

final User user = (await _auth.signInWithCredential(facebookAuthCred)).user;

return user;

}



Future<void> logout() async {

await _googleSignIn.disconnect();

await _auth.signOut();

await _facebookLogin.logOut();

}

}



class UserSocialData {

UserSocialData._({this.name, this.email, this.photoUrl});



factory UserSocialData.fromApple(AuthorizationResult result, User user) {

final AppleIdCredential appleIdCredential = result.credential;

final PersonNameComponents nameComponents = appleIdCredential.fullName;

final String name = <String>[

nameComponents.namePrefix,

nameComponents.givenName,

nameComponents.middleName,

nameComponents.familyName,

nameComponents.nameSuffix,

].where((String element) => element != null).join(' ') ??

'Apple ID';



final String email =

appleIdCredential.email ?? user?.providerData?.first?.email;

return UserSocialData._(name: name, email: email, photoUrl: null);

}



factory UserSocialData.fromGoogle(GoogleSignInAccount account) {

return UserSocialData._(

name: account.displayName,

email: account.email,

photoUrl: account.photoUrl,

);

}



factory UserSocialData.fromFacebook(UserCredential result) {

final Map<String, dynamic> profile = result.additionalUserInfo.profile;

final String name = profile['name'] as String ?? result.user.displayName;

final String email = profile['email'] as String ?? result.user.email;

final String pictureData =

(profile['picture'] as Map<dynamic, dynamic>)['data'] as String;

final String photoUrl =

(pictureData as Map<dynamic, dynamic>)['url'] as String;

return UserSocialData._(name: name, email: email, photoUrl: photoUrl);

}



final String name;

final String email;

final String photoUrl;

}

@nhancv