Skip to content

Instantly share code, notes, and snippets.

@nhancv
Last active July 6, 2021 14:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhancv/0bd216958bb8e589e50dc346f6137274 to your computer and use it in GitHub Desktop.
Save nhancv/0bd216958bb8e589e50dc346f6137274 to your computer and use it in GitHub Desktop.
Flutter firebase setup

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

...
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'
#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>;
}
<?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>
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
}
<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>
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)
}
}
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();
}
}
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');
}
}
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);
}
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:nft/services/app/app_loading.dart';
import 'package:nft/services/app/dynamic_size.dart';
import 'package:nft/services/firebase/cloud_firestore.dart';
import 'package:nft/services/firebase/cloud_functions.dart';
import 'package:nft/services/firebase/cloud_storage.dart';
import 'package:nft/services/firebase/firebase_error.dart';
import 'package:nft/services/firebase/firebase_error_type.dart';
import 'package:nft/services/firebase/firebase_login.dart';
import 'package:nft/services/firebase/local_push.dart';
import 'package:nft/utils/app_helper.dart';
import 'package:nft/utils/app_log.dart';
import 'package:nft/utils/app_style.dart';
import 'package:nft/widgets/p_appbar_empty.dart';
import 'package:nft/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);
}
}
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();
}
}
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}';
}
}
import 'dart:async';
import 'package:apple_sign_in/apple_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_facebook_auth/flutter_facebook_auth.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 FacebookAuth _facebookAuth = FacebookAuth.instance;
Future<bool> isAppleSignInAvailable() => AppleSignIn.isAvailable();
Future<User> signInGoogle() async {
await logout();
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 {
await logout();
final LoginResult result = await _facebookAuth.login(loginBehavior: LoginBehavior.WEB_VIEW_ONLY);
final String accessToken = result?.accessToken?.token;
if (accessToken == null) {
return null;
}
final AuthCredential facebookAuthCred = FacebookAuthProvider.credential(accessToken);
final User user = (await _auth.signInWithCredential(facebookAuthCred)).user;
return user;
}
Future<void> logout() async {
try {
if (await _googleSignIn.isSignedIn()) {
await _googleSignIn.disconnect();
}
if (await _facebookAuth.accessToken != null) {
await _facebookAuth.logOut();
}
await _auth.signOut();
} catch (e) {
logger.e(e);
}
}
}
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;
}
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:nft/utils/app_log.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();
logger.d('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) {
logger.d('User granted permission');
////////////////////////////////////////////////////////////////////
// Get any messages which caused the application to open from
// a terminated state.
final RemoteMessage initialMessage = await FirebaseMessaging.instance.getInitialMessage();
// logger.d('initialMessage: ${initialMessage?.notification?.title}');
// Also handle any interaction when the app is in the background via a
// Stream listener
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
// logger.d('A new onMessageOpenedApp event was published!');
// logger.d('onMessageOpenedApp: ${message?.notification?.title}');
});
// Listen new message
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
logger.d('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 getFcmToken();
logger.d('fcmToken: {start}$fcmToken{end}');
} else if (settings.authorizationStatus == AuthorizationStatus.provisional) {
logger.d('User granted provisional permission');
} else {
logger.d('User declined or has not accepted permission');
}
}
Future<String> getFcmToken() async {
try {
final String fcmToken = await _messaging.getToken();
return fcmToken;
} catch (e) {
// Some time Firebase return map not string
}
return null;
}
}
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
// Debug view: https://firebase.google.com/docs/analytics/debugview
// Enable: adb shell setprop debug.firebase.analytics.app package_name
// Disable: adb shell setprop debug.firebase.analytics.app .none.
// --
// The event name can only contain alphanumeric characters and underscores,
// with an alphabetic as the first character. The maximum length is 40
class FirebaseTracking {
FirebaseTracking._() {
disableLog = kDebugMode;
}
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);
}
}
<?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>
<?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>
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 const String DEFAULT_SOUND = 'default';
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(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
onDidReceiveLocalNotification: onDidReceiveLocalNotification,
);
final InitializationSettings initializationSettings =
InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS);
return flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
}
/// Request notification permission
Future<void> requestNotificationPermission() async {
if (Platform.isIOS) {
await FlutterLocalNotificationsPlugin()
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
}
/// 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();
}
}
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],
);
}
}
# 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.14
flutter_local_notifications: ^3.0.1+6
firebase_auth: ^0.18.3+1
google_sign_in:
git:
url: https://github.com/nhancv/google_sign_in_f1.git
path: packages/google_sign_in/google_sign_in
ref: master
apple_sign_in: ^0.1.0
flutter_facebook_auth: ^3.3.3-no-nullsafety
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment