diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8f5f664 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,63 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + FLUTTER_VERSION: "3" + JAVA_VERSION: "11" + +jobs: + build-android: + name: Build Android + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: ${{ env.JAVA_VERSION }} + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Create .env + run: | + echo "FRONT_ADDRESS=https://taxi.sparcs.org" > .env + echo "BACK_ADDRESS=https://taxi.sparcs.org" >> .env + - name: Create key.properties + run: | + echo "UPLOAD_STORE_FILE=../ci.jks" > android/key.properties + echo "UPLOAD_STORE_PASSWORD=123456" >> android/key.properties + echo "UPLOAD_KEY_PASSWORD=123456" >> android/key.properties + echo "UPLOAD_KEY_ALIAS=ci" >> android/key.properties + - name: Create google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' | base64 --decode > android/app/google-services.json + - name: Install dependencies + run: flutter pub get + - name: Build APK + run: flutter build apk --release + + build-ios: + name: Build iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Create .env + run: | + echo "FRONT_ADDRESS=https://taxi.sparcs.org" > .env + echo "BACK_ADDRESS=https://taxi.sparcs.org" >> .env + - name: Create GoogleService-Info.plist + run: echo '${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}' | base64 --decode > ios/Runner/GoogleService-Info.plist + - name: Install dependencies + run: flutter pub get + - name: Build iOS + run: flutter build ios --release --no-codesign diff --git a/android/.gitignore b/android/.gitignore index 6f56801..7d27f31 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks +!/ci.jks diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 12a0001..8a76fc3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - @@ -11,6 +11,11 @@ + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/taxi_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/taxi_app/MainActivity.kt index bfe5e35..fee0a26 100644 --- a/android/app/src/main/kotlin/com/example/taxi_app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/taxi_app/MainActivity.kt @@ -22,6 +22,7 @@ class MainActivity: FlutterActivity() { val intent = Intent.parseUri(call.arguments as String, Intent.URI_INTENT_SCHEME) if (intent.resolveActivity(packageManager) != null) { + packageManager.getLaunchIntentForPackage(""+intent.getPackage()) startActivity(intent) result.success(null) } else { diff --git a/android/app/src/main/res/xml/filepaths.xml b/android/app/src/main/res/xml/filepaths.xml new file mode 100644 index 0000000..c0f3b31 --- /dev/null +++ b/android/app/src/main/res/xml/filepaths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 88720e4..9f980fc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() diff --git a/android/ci.jks b/android/ci.jks new file mode 100644 index 0000000..edcba2b Binary files /dev/null and b/android/ci.jks differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bfcca70..8533716 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -160,7 +160,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..a6b826d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ ITSAppUsesNonExemptEncryption + FacebookAppID + 278480901639073 LSApplicationQueriesSchemes itms-beta itms + instagram-stories + facebook-stories + facebook + instagram + twitter + whatsapp + tg + supertoss + uber + tmoneyonda + kakaotalk + kakaot LSRequiresIPhoneOS diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart deleted file mode 100644 index 81b3bf7..0000000 --- a/lib/constants/constants.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:dio/dio.dart"; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:taxiapp/utils/remoteConfigController.dart'; - -String address = RemoteConfigController().backUrl; - -final BaseOptions connectionOptions = BaseOptions( - baseUrl: address, - connectTimeout: Duration(seconds: 150), - receiveTimeout: Duration(seconds: 130), -); diff --git a/lib/constants/theme.dart b/lib/constants/theme.dart new file mode 100644 index 0000000..2870a0e --- /dev/null +++ b/lib/constants/theme.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +//primaryColor 지정 (색상코드: #6E3647) +final Map primaryColor1 = { + 50: const Color.fromRGBO(110, 54, 120, .1), + 100: const Color.fromRGBO(110, 54, 120, .2), + 200: const Color.fromRGBO(110, 54, 120, .3), + 300: const Color.fromRGBO(110, 54, 120, .4), + 400: const Color.fromRGBO(110, 54, 120, .5), + 500: const Color.fromRGBO(110, 54, 120, .6), + 600: const Color.fromRGBO(110, 54, 120, .7), + 700: const Color.fromRGBO(110, 54, 120, .8), + 800: const Color.fromRGBO(110, 54, 120, .9), + 900: const Color.fromRGBO(110, 54, 120, 1), +}; +final MaterialColor taxiPrimaryMaterialColor = + MaterialColor(0xFF6E3678, primaryColor1); +const Color taxiPrimaryColor = Color(0xFF6E3678); +const Color taxiMainBackgroundColor = Colors.white; +const Color toastBackgroundColor = Colors.white; +const Color toastTextColor = Colors.black; +const Color notiColor = Color(0x66C8C8C8); +final Color dialogBarrierColor = Colors.black.withOpacity(0.6); + +double devicePixelRatio = 3.0; +const double taxiDialogPadding = 15.0; +const double taxiNotificationPadding = 20.0; +final defaultDialogUpperTitlePadding = + Padding(padding: EdgeInsets.symmetric(vertical: 36.0 / devicePixelRatio)); + +final defaultDialogMedianTitlePadding = + Padding(padding: EdgeInsets.all(6 / devicePixelRatio)); + +final defaultDialogLowerTitlePadding = + Padding(padding: EdgeInsets.symmetric(vertical: 24 / devicePixelRatio)); + +final defaultDialogVerticalMedianButtonPadding = Padding( + padding: + EdgeInsets.symmetric(horizontal: taxiDialogPadding / devicePixelRatio)); + +final defaultDialogLowerButtonPadding = Padding( + padding: + EdgeInsets.only(bottom: (taxiDialogPadding / 2) / devicePixelRatio)); + +final defaultDialogPadding = + Padding(padding: EdgeInsets.all(taxiDialogPadding / devicePixelRatio)); + +final defaultDialogButtonSize = Size(147.50, 35); + +final defaultDialogButtonInnerPadding = EdgeInsets.only(top: 9, bottom: 9); + +final defaultDialogButtonBorderRadius = BorderRadius.circular(8.0); + +final defaultTaxiMarginDouble = 20.0; + +final defaultTaxiMargin = + EdgeInsets.symmetric(horizontal: defaultTaxiMarginDouble); + +const defaultNotificationButtonSize = Size(90, 25); +const defaultNotificationButtonInnerPadding = + EdgeInsets.symmetric(horizontal: 15.0, vertical: 2.0); +final defaultNotificationButtonBorderRadius = BorderRadius.circular(30.0); +final defaultNotificatonOutlinedButtonStyle = OutlinedButton.styleFrom( + minimumSize: Size.zero, + fixedSize: defaultNotificationButtonSize, + padding: defaultNotificationButtonInnerPadding, + backgroundColor: taxiPrimaryMaterialColor, + shape: RoundedRectangleBorder( + borderRadius: defaultNotificationButtonBorderRadius, + side: const BorderSide(color: Colors.black), + ), +); // TODO: ThemeData에 있는 OutlinedButtonThemeData 분리 + +ThemeData taxiTheme() { + final base = ThemeData( + primarySwatch: taxiPrimaryMaterialColor, + primaryColor: const Color(0xFF6E3678), + + //dialog 테마 + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + backgroundColor: Colors.white, + actionsPadding: const EdgeInsets.all(10.0), + surfaceTintColor: Colors.black, + ), + dialogBackgroundColor: Colors.white, + + //dialog 버튼 + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0.5, + fixedSize: defaultDialogButtonSize, + padding: defaultDialogButtonInnerPadding, + backgroundColor: const Color.fromARGB(255, 238, 238, 238), + shape: RoundedRectangleBorder( + borderRadius: defaultDialogButtonBorderRadius, + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + fixedSize: defaultDialogButtonSize, + padding: defaultDialogButtonInnerPadding, + backgroundColor: taxiPrimaryMaterialColor, + shape: RoundedRectangleBorder( + borderRadius: defaultDialogButtonBorderRadius, + side: const BorderSide(color: Colors.black), + ), + ), + ), + bannerTheme: MaterialBannerThemeData( + backgroundColor: Colors.white, + ), + + //텍스트 테마 + textTheme: const TextTheme( + //Dialog 제목 + titleSmall: TextStyle( + fontFamily: 'NanumSquare', + color: Color(0xFF323232), + fontSize: 16, + fontWeight: FontWeight.w400), + + //Dialog 상세 설명 + bodySmall: TextStyle( + fontFamily: 'NanumSquare_acB', + color: Color(0xFF888888), + fontSize: 10, + fontWeight: FontWeight.w700), + + //Dialog Outlined 버튼 텍스트 + labelLarge: TextStyle( + fontFamily: 'NanumSquare_acB', + color: Color(0xFFEEEEEE), + fontSize: 14, + fontWeight: FontWeight.w700), + + //Dialog Elevated 버튼 텍스트 + labelMedium: TextStyle( + fontFamily: 'NanumSquare', + color: Color.fromARGB(255, 129, 129, 129), + fontSize: 14, + fontWeight: FontWeight.w400), + labelSmall: TextStyle( + color: Color(0xFFEEEEEE), + fontFamily: 'NanumSquare_acB', + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.4, + ), + ), + + bottomNavigationBarTheme: BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + backgroundColor: Colors.white, + selectedItemColor: Color(0xFF6E3678), + selectedLabelStyle: TextStyle( + fontFamily: 'NanumSquare', + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.4, + ), + unselectedLabelStyle: TextStyle( + fontFamily: 'NanumSquare', + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.4, + ), + ), + ); + return base; +} diff --git a/lib/main.dart b/lib/main.dart index f408d7c..40b3bd9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:taxiapp/utils/token.dart'; import 'package:taxiapp/views/taxiView.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:taxiapp/firebase_options.dart'; +import 'constants/theme.dart'; late AndroidNotificationChannel channel; late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; @@ -82,13 +83,11 @@ class MyHome extends HookWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Taxi App', - theme: ThemeData( - primarySwatch: Colors.blue, - ), + theme: taxiTheme(), home: Container( - color: const Color(0xFF6E3647), + color: Theme.of(context).primaryColor, child: Container( - color: Colors.white, + color: taxiMainBackgroundColor, child: TaxiView(), ), ), diff --git a/lib/utils/fcmToken.dart b/lib/utils/fcmToken.dart index c591753..dbedeb6 100644 --- a/lib/utils/fcmToken.dart +++ b/lib/utils/fcmToken.dart @@ -1,13 +1,13 @@ import "package:dio/dio.dart"; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:taxiapp/constants/constants.dart'; +import 'package:taxiapp/utils/remoteConfigController.dart'; class FcmToken { String token; static FcmToken? _instance; - final Dio _dio = Dio(connectionOptions); + final Dio _dio = Dio(); FcmToken._internal({required this.token}); @@ -21,6 +21,15 @@ class FcmToken { Future init() async { final token = await FirebaseMessaging.instance.getToken(); + _dio.interceptors + .add(InterceptorsWrapper(onRequest: (options, handler) async { + options.headers["Origin"] = options.uri.origin; + return handler.next(options); + }, onResponse: (response, handler) async { + return handler.next(response); + }, onError: (error, handler) async { + return handler.next(error); + })); if (token == null) { this.token = ''; @@ -32,6 +41,7 @@ class FcmToken { String get fcmToken => token; Future registerToken(String accessToken) async { + _dio.options.baseUrl = RemoteConfigController().backUrl; return _dio.post("auth/app/device", data: { "accessToken": accessToken, "deviceToken": token, @@ -43,6 +53,7 @@ class FcmToken { } Future removeToken(String accessToken) async { + _dio.options.baseUrl = RemoteConfigController().backUrl; return _dio.delete("auth/app/device", data: { "accessToken": accessToken, "deviceToken": token, diff --git a/lib/utils/pushHandler.dart b/lib/utils/pushHandler.dart index bb8c0ae..f92094f 100644 --- a/lib/utils/pushHandler.dart +++ b/lib/utils/pushHandler.dart @@ -33,11 +33,9 @@ Future handleMessage(RemoteMessage message) async { var details = NotificationDetails(android: androidNotiDetails, iOS: iOSNotiDetails); - if (message.data != null) { - flutterLocalNotificationsPlugin.show(Random().nextInt(100000000), - message.data['title'], message.data['body'], details, - payload: message.data['url']); - } + flutterLocalNotificationsPlugin.show(Random().nextInt(100000000), + message.data['title'], message.data['body'], details, + payload: message.data['url']); } Future _getByteArrayFromUrl(String url) async { diff --git a/lib/utils/remoteConfigController.dart b/lib/utils/remoteConfigController.dart index 2754eab..ade4ee4 100644 --- a/lib/utils/remoteConfigController.dart +++ b/lib/utils/remoteConfigController.dart @@ -1,13 +1,11 @@ -import "package:dio/dio.dart"; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:package_info/package_info.dart'; class RemoteConfigController { String backUrl; String frontUrl; - String ios_version; - String android_version; + String iosVersion; + String androidVersion; static RemoteConfigController? _instance; @@ -16,34 +14,28 @@ class RemoteConfigController { RemoteConfigController._internal( {required this.backUrl, required this.frontUrl, - required this.ios_version, - required this.android_version}); + required this.iosVersion, + required this.androidVersion}); factory RemoteConfigController( {String? backUrl, String? frontUrl, - String? ios_version, - String? android_version}) { + String? iosVersion, + String? androidVersion}) { if (frontUrl == null || backUrl == null || - ios_version == null || - android_version == null) { + iosVersion == null || + androidVersion == null) { return _instance ??= RemoteConfigController._internal( - backUrl: 'https://taxi.sparcs.org/api/', - frontUrl: 'https://taxi.sparcs.org', - ios_version: '', - android_version: ''); + backUrl: '', frontUrl: '', iosVersion: '', androidVersion: ''); } _instance = RemoteConfigController._internal( backUrl: backUrl, frontUrl: frontUrl, - ios_version: ios_version, - android_version: android_version); + iosVersion: iosVersion, + androidVersion: androidVersion); return _instance ??= RemoteConfigController._internal( - backUrl: 'https://taxi.sparcs.org/api/', - frontUrl: 'https://taxi.sparcs.org', - ios_version: '', - android_version: ''); + backUrl: '', frontUrl: '', iosVersion: '', androidVersion: ''); } Future init() async { @@ -55,7 +47,7 @@ class RemoteConfigController { )); await remoteConfig.setDefaults({ "back_url": "https://taxi.sparcs.org/api/", - "front_url": "https://taxi.sparcs.org", + "front_url": "https://taxi.sparcs.org/", "version": value.version, "ios_version": value.version, }); @@ -64,8 +56,8 @@ class RemoteConfigController { this.backUrl = remoteConfig.getString("back_url"); this.frontUrl = remoteConfig.getString("front_url"); - this.android_version = remoteConfig.getString("version"); - this.ios_version = remoteConfig.getString("ios_version"); + this.androidVersion = remoteConfig.getString("version"); + this.iosVersion = remoteConfig.getString("ios_version"); return; } diff --git a/lib/utils/token.dart b/lib/utils/token.dart index c58c12b..31e5b1a 100644 --- a/lib/utils/token.dart +++ b/lib/utils/token.dart @@ -1,11 +1,11 @@ import 'dart:io'; import "package:dio/dio.dart"; -import 'package:taxiapp/constants/constants.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:taxiapp/utils/fcmToken.dart'; +import 'package:taxiapp/utils/remoteConfigController.dart'; class Token { String accessToken; @@ -13,7 +13,7 @@ class Token { static Token? _instance; static final _storage = FlutterSecureStorage(); - final Dio _dio = Dio(connectionOptions); + final Dio _dio = Dio(); final CookieJar _cookieJar = CookieJar(); Token._internal({required this.accessToken, required this.refreshToken}); @@ -30,6 +30,16 @@ class Token { Future init() async { accessToken = (await getAccessTokenFromStorage()) ?? ''; refreshToken = (await getRefreshTokenFromStorage()) ?? ''; + _dio.interceptors.add(CookieManager(_cookieJar)); + _dio.interceptors + .add(InterceptorsWrapper(onRequest: (options, handler) async { + options.headers["Origin"] = options.uri.origin; + return handler.next(options); + }, onResponse: (response, handler) async { + return handler.next(response); + }, onError: (error, handler) async { + return handler.next(error); + })); } Future setAccessToken({required String accessToken}) async { @@ -56,6 +66,7 @@ class Token { } Future getSession() async { + _dio.options.baseUrl = RemoteConfigController().backUrl; _dio.interceptors.add(CookieManager(_cookieJar)); return _dio.get("/auth/app/token/login", queryParameters: { "accessToken": accessToken, @@ -74,8 +85,8 @@ class Token { return null; } if (response.statusCode == 200) { - List cookies = await _cookieJar.loadForRequest( - Uri.parse(connectionOptions.baseUrl + "auth/app/token/login")); + List cookies = await _cookieJar.loadForRequest(Uri.parse( + RemoteConfigController().backUrl + "auth/app/token/login")); for (Cookie cookie in cookies) { if (cookie.name == "connect.sid") { return cookie.value; @@ -90,6 +101,7 @@ class Token { } Future updateAccessTokenUsingRefreshToken() { + _dio.options.baseUrl = RemoteConfigController().backUrl; return _dio.get("/auth/app/token/refresh", queryParameters: { "accessToken": accessToken, "refreshToken": refreshToken, diff --git a/lib/views/taxiDialog.dart b/lib/views/taxiDialog.dart index 5d91e70..2f7aa70 100644 --- a/lib/views/taxiDialog.dart +++ b/lib/views/taxiDialog.dart @@ -2,91 +2,85 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:open_store/open_store.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:taxiapp/constants/theme.dart'; class TaxiDialog extends StatelessWidget { - late Set boxContent; + late Set boxMainContent; + late Set boxSecondaryContent; late String leftButtonContent; late String rightButtonContent; TaxiDialog( {super.key, - required this.boxContent, + required this.boxMainContent, + required this.boxSecondaryContent, required this.leftButtonContent, required this.rightButtonContent}); @override Widget build(BuildContext context) { return Container( - width: 350, - height: 165, - decoration: BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.circular(15)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Padding(padding: EdgeInsets.all(15)), - ...boxContent, - const Padding( - padding: EdgeInsets.all(15), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0.5, - fixedSize: const Size(150, 45), - backgroundColor: const Color(0xFFFAF8FB), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - side: const BorderSide(color: Colors.white), - ), - ), - child: Text(leftButtonContent, - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFFC8C8C8), - fontSize: 13, - fontWeight: FontWeight.normal))), - onPressed: () async { - if (Platform.isIOS) { - exit(0); - } else { - SystemNavigator.pop(); - } - }), - const Padding( - padding: EdgeInsets.all(10), - ), - OutlinedButton( - style: ButtonStyle( - fixedSize: MaterialStateProperty.all(const Size(150, 45)), - backgroundColor: MaterialStateProperty.all( - const Color(0xFF6E3678)), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - side: const BorderSide(color: Colors.black), + height: double.infinity, + width: double.infinity, + color: dialogBarrierColor, + child: Dialog( + alignment: Alignment.center, + backgroundColor: Theme.of(context).dialogBackgroundColor, + shape: Theme.of(context).dialogTheme.shape, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + defaultDialogUpperTitlePadding, + ...boxMainContent, + defaultDialogMedianTitlePadding, + ...boxSecondaryContent, + defaultDialogLowerTitlePadding, + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + defaultDialogPadding, + Expanded( + child: ElevatedButton( + style: Theme.of(context).elevatedButtonTheme.style, + child: Text( + leftButtonContent, + style: Theme.of(context).textTheme.labelMedium, + overflow: TextOverflow.visible, ), - ), - ), - child: Text(rightButtonContent, - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFFEEEEEE), - fontSize: 13, - fontWeight: FontWeight.bold))), - onPressed: () async { - OpenStore.instance.open( - androidAppBundleId: dotenv.get("ANDROID_APPID"), - appStoreId: dotenv.get("IOS_APPID")); - }), - ], - ) - ]), + onPressed: () async { + if (Platform.isIOS) { + exit(0); + } else { + SystemNavigator.pop(); + } + }), + ), + defaultDialogVerticalMedianButtonPadding, + Expanded( + child: OutlinedButton( + style: Theme.of(context).outlinedButtonTheme.style, + child: Text( + rightButtonContent, + style: Theme.of(context).textTheme.labelLarge, + overflow: TextOverflow.visible, + ), + onPressed: () async { + OpenStore.instance.open( + androidAppBundleId: dotenv.get("ANDROID_APPID"), + appStoreId: dotenv.get("IOS_APPID")); + }), + ), + defaultDialogPadding, + ], + ), + defaultDialogLowerButtonPadding + ]), + ), ); } } diff --git a/lib/views/taxiView.dart b/lib/views/taxiView.dart index 0af51c2..03661f9 100644 --- a/lib/views/taxiView.dart +++ b/lib/views/taxiView.dart @@ -1,16 +1,18 @@ import 'dart:async'; import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; -import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:package_info/package_info.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:social_share/social_share.dart'; +import 'package:taxiapp/constants/theme.dart'; import 'package:taxiapp/utils/fcmToken.dart'; import 'package:taxiapp/utils/pushHandler.dart'; import 'package:taxiapp/utils/remoteConfigController.dart'; @@ -21,17 +23,27 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:taxiapp/views/taxiDialog.dart'; import 'package:app_links/app_links.dart'; +import 'package:taxiapp/constants/theme.dart'; +import 'dart:math'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:open_store/open_store.dart'; class TaxiView extends HookWidget { final CookieManager _cookieManager = CookieManager.instance(); // late InAppWebViewController _controller; - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); @override Widget build(BuildContext context) { String address = RemoteConfigController().frontUrl; + OverlayEntry? overlayEntry; + AnimationController _aniController = + useAnimationController(duration: const Duration(milliseconds: 300)); + Animation _animation = + Tween(begin: const Offset(0, -0.5), end: const Offset(0.0, 0)).animate( + CurvedAnimation(parent: _aniController, curve: Curves.decelerate)); + bool isBannerShow = false; // States // 로딩 여부 확인 @@ -61,6 +73,8 @@ class TaxiView extends HookWidget { // FCM init 여부 확인 final isFcmInit = useState(false); + devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + useEffect(() { if (isTimerUp.value) { FcmToken().init().then((value) { @@ -207,13 +221,13 @@ class TaxiView extends HookWidget { try { if (Platform.isIOS) { if (int.parse( - RemoteConfigController().ios_version.replaceAll(".", "")) > + RemoteConfigController().iosVersion.replaceAll(".", "")) > int.parse(value.version.replaceAll(".", ""))) { isMustUpdate.value = true; } } else { if (int.parse(RemoteConfigController() - .android_version + .androidVersion .replaceAll(".", "")) > int.parse(value.version.replaceAll(".", ""))) { isMustUpdate.value = true; @@ -222,9 +236,9 @@ class TaxiView extends HookWidget { } catch (e) { Fluttertoast.showToast( msg: "버전 체크에 실패했습니다. " + e.toString(), - backgroundColor: Colors.white, + backgroundColor: toastBackgroundColor, toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, + textColor: toastTextColor, ); } }); @@ -238,8 +252,11 @@ class TaxiView extends HookWidget { if (Token().accessToken != '') { await Token().deleteAll(); } - isLogin.value = false; + await _cookieManager.deleteCookie( + url: Uri.parse(RemoteConfigController().backUrl), + name: "connect.sid"); isAuthLogin.value = false; + isLogin.value = false; isFirstLogin.value = false; LoadCount.value += 1; } else { @@ -255,12 +272,11 @@ class TaxiView extends HookWidget { LoadCount.value += 1; } } catch (e) { - print(e); Fluttertoast.showToast( msg: "로그인에 실패했습니다.", - backgroundColor: Colors.white, + backgroundColor: toastBackgroundColor, toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, + textColor: toastTextColor, ); } } @@ -269,6 +285,156 @@ class TaxiView extends HookWidget { return; }, [isAuthLogin.value, isFcmInit.value]); + void removeOverlayNotification({required Uri? uri}) { + if (uri != Uri.parse("")) { + url.value = uri.toString(); + LoadCount.value += 1; + } + overlayEntry?.remove(); + overlayEntry = null; + } + + void removeAnimation() { + _aniController.reverse(); //TODO: 일정 dy 미만시 배너 삭제 취소 및 애니메이션 다시 재생 + isBannerShow = false; + // removeOverlayNotification(); + } + + void createOverlayNotification( + {required String title, + required String subTitle, + required String content, + required Map button, + Uri? imageUrl}) { + print("asd"); + if (overlayEntry != null) { + removeOverlayNotification(uri: Uri.parse("")); + } + assert(overlayEntry == null); + isBannerShow = true; + + overlayEntry = OverlayEntry(builder: (BuildContext context) { + _aniController.reset(); + _animation = + Tween(begin: const Offset(0, -0.5), end: const Offset(0, 0)) + .animate(CurvedAnimation( + parent: _aniController, curve: Curves.decelerate)); + _aniController.forward(); + + return SlideTransition( + position: _animation, + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -1 && isBannerShow) { + removeAnimation(); + } + }, + onPanEnd: (details) { + if (!isBannerShow) { + removeOverlayNotification(uri: button.values.first); + } + }, + child: UnconstrainedBox( + alignment: Alignment.topCenter, + child: Container( + width: MediaQuery.of(context).size.width, + height: min(MediaQuery.of(context).size.height * 0.15, 200), + margin: + EdgeInsets.only(top: MediaQuery.of(context).padding.top), + color: Colors.white, + child: Stack( + children: [ + Container( + alignment: Alignment.topCenter, + height: 5.0, + color: taxiPrimaryColor, + ), + Positioned( + left: 20, + top: 25, + child: (imageUrl != Uri.parse("")) + ? Image( + image: NetworkImage(imageUrl.toString()), + width: 40, + height: 40, + fit: BoxFit.cover, + ) + : const Padding(padding: EdgeInsets.zero)), + Positioned( + left: 20 + + ((imageUrl != Uri.parse("")) + ? 60 + : 0), // 이미지 없을 시 마진 20으로 변경 + top: 25, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: title, + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontSize: 12, + ), + ), + TextSpan( + text: + (subTitle.isNotEmpty) ? " / $subTitle" : "", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontSize: 12, + fontWeight: FontWeight.w400)), + ], + ), + ), + ), + Positioned( + left: 20 + ((imageUrl != Uri.parse("")) ? 60 : 0), + top: 40, + child: Text( + content, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w400, + letterSpacing: 0.4), + ), + ), + Positioned( + bottom: 20 / devicePixelRatio, + right: 25 / devicePixelRatio, + child: OutlinedButton( + style: defaultNotificatonOutlinedButtonStyle, + child: Text( + button.keys.first, + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(fontSize: 14), + ), + onPressed: () { + removeAnimation(); + Future.delayed(const Duration(milliseconds: 300), + () { + removeOverlayNotification( + uri: button.values.first); + }); + }), + ), + ], + ), + ), + ), + ), + ); + }); + Overlay.of(context).insert(overlayEntry!); + } + return SafeArea( child: Stack(children: [ WillPopScope( @@ -279,12 +445,46 @@ class TaxiView extends HookWidget { initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( useShouldOverrideUrlLoading: true, - resourceCustomSchemes: ['intent']), + applicationNameForUserAgent: "taxi-app-webview/" + + (Platform.isAndroid ? "android" : "ios"), + resourceCustomSchemes: [ + 'intent', + 'supertoss', + 'uber', + 'tmoneyonda', + 'kakaotalk', + 'kakaot' + ]), android: AndroidInAppWebViewOptions( useHybridComposition: true, overScrollMode: - AndroidOverScrollMode.OVER_SCROLL_NEVER)), + AndroidOverScrollMode.OVER_SCROLL_NEVER), + ios: IOSInAppWebViewOptions(disallowOverScroll: true)), // initialUrlRequest: URLRequest(url: Uri.parse(address)), + shouldOverrideUrlLoading: (controller, navigationAction) async { + var newHeaders = Map.from( + navigationAction.request.headers ?? {}); + if (Platform.isAndroid && + !newHeaders.containsKey("Referer") && + navigationAction.request.url.toString() != + 'about:blank' && + (navigationAction.request.url?.origin == + Uri.parse(address).origin || + navigationAction.request.url?.origin == + Uri.parse(RemoteConfigController().backUrl) + .origin)) { + newHeaders['Referer'] = + navigationAction.request.url.toString(); + newHeaders['Origin'] = RemoteConfigController().frontUrl; + var newRequest = navigationAction.request; + newRequest.headers = newHeaders; + await controller.loadUrl(urlRequest: newRequest); + + return NavigationActionPolicy.CANCEL; + } + + return NavigationActionPolicy.ALLOW; + }, onWebViewCreated: (InAppWebViewController webcontroller) async { _controller.value = webcontroller; _controller.value?.addJavaScriptHandler( @@ -297,14 +497,17 @@ class TaxiView extends HookWidget { } // 로그인 성공 시 / 기존 토큰 삭제 후 새로운 토큰 저장 if (!isAuthLogin.value) { - print("IS AUTH 변경됨"); - await Token().setAccessToken( - accessToken: arguments[0]['accessToken']); - await Token().setRefreshToken( - refreshToken: arguments[0]['refreshToken']); - await FcmToken() - .registerToken(arguments[0]['accessToken']); - isAuthLogin.value = true; + if (arguments[0]['accessToken'] != null && + arguments[0]['refreshToken'] != null) { + await Token().deleteAll(); + await Token().setAccessToken( + accessToken: arguments[0]['accessToken']); + await Token().setRefreshToken( + refreshToken: arguments[0]['refreshToken']); + await FcmToken() + .registerToken(arguments[0]['accessToken']); + isAuthLogin.value = true; + } } return; }, @@ -317,18 +520,21 @@ class TaxiView extends HookWidget { await FcmToken() .removeToken(Token().getAccessToken()); await Token().deleteAll(); + await _cookieManager.deleteAllCookies(); isLogin.value = false; isAuthLogin.value = false; - await _cookieManager.deleteAllCookies(); - await _controller.value!.loadUrl( - urlRequest: URLRequest(url: Uri.parse(address))); + await _controller.value?.loadUrl( + urlRequest: URLRequest( + url: Uri.parse(RemoteConfigController() + .frontUrl + .toString()))); } catch (e) { // TODO Fluttertoast.showToast( msg: "서버와의 연결에 실패했습니다.", toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); isAuthLogin.value = false; } }); @@ -343,8 +549,8 @@ class TaxiView extends HookWidget { Fluttertoast.showToast( msg: "알림 권한을 허용해주세요.", toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); return false; } }); @@ -356,6 +562,69 @@ class TaxiView extends HookWidget { await Clipboard.setData(ClipboardData(text: args[0])); } }); + + // Web -> App + _controller.value?.addJavaScriptHandler( + handlerName: "popup_inAppNotification", + callback: (args) async { + createOverlayNotification( + title: args[0]['title'].toString(), + subTitle: args[0]['subtitle'].toString(), + content: args[0]['content'].toString(), + button: { + args[0]['button']['text'].toString(): + (args[0]['button']['path'].toString() != "") + ? Uri.parse( + args[0]['button']['path'].toString()) + : Uri.parse("") + }, + imageUrl: (args[0]['type'].toString() == + "default") //TODO: type showMaterialBanner 함수에서 관리 + ? Uri.parse(args[0]['imageUrl'].toString()) + : Uri.parse("")); + }); + + _controller.value?.addJavaScriptHandler( + handlerName: "popup_instagram_story_share", + callback: (args) async { + if (args[0] == {}) { + return; + } + print(args); + try { + final Dio _dio = Dio(); + final backgroundResponse = await _dio.get( + args[0]['backgroundLayerUrl'], + options: + Options(responseType: ResponseType.bytes)); + final stickerResponse = await _dio.get( + args[0]['stickerLayerUrl'], + options: + Options(responseType: ResponseType.bytes)); + final backgroundFile = await File( + (await getTemporaryDirectory()).path + + "/background.png") + .create(recursive: true); + final stickerFile = await File( + (await getTemporaryDirectory()).path + + "/sticker.png") + .create(recursive: true); + await backgroundFile + .writeAsBytes(backgroundResponse.data); + await stickerFile.writeAsBytes(stickerResponse.data); + + await SocialShare.shareInstagramStory( + appId: dotenv.get("FACEBOOK_APPID"), + imagePath: stickerFile.path, + backgroundResourcePath: backgroundFile.path); + } catch (e) { + Fluttertoast.showToast( + msg: "인스타그램 스토리 공유에 실패했습니다.", + toastLength: Toast.LENGTH_SHORT, + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); + } + }); }, onLoadStart: (controller, uri) async { if (isFcmInit.value && @@ -363,60 +632,77 @@ class TaxiView extends HookWidget { sessionToken.value != '' && uri?.origin == Uri.parse(address).origin && (await _cookieManager.getCookie( - url: Uri.parse(address), name: "connect.sid")) + url: Uri.parse( + RemoteConfigController().backUrl), + name: "connect.sid")) ?.value != sessionToken.value) { try { await _controller.value?.stopLoading(); await _cookieManager.deleteCookie( - url: Uri.parse(address), name: "connect.sid"); + url: Uri.parse(RemoteConfigController().backUrl), + name: "connect.sid"); await _cookieManager.setCookie( - url: Uri.parse(address), + url: Uri.parse(RemoteConfigController().backUrl), name: "connect.sid", value: sessionToken.value, ); - await _cookieManager.setCookie( - url: Uri.parse(address), - name: "deviceToken", - value: FcmToken().fcmToken, - ); await _controller.value?.reload(); } catch (e) { // TODO : handle error Fluttertoast.showToast( msg: "서버와의 연결에 실패했습니다.", toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); isAuthLogin.value = false; } } }, - onUpdateVisitedHistory: - (controller, url, androidIsReload) async { - // 로그아웃 링크 감지 - if (url.toString().contains("logout") && isAuthLogin.value) { + onLoadResourceCustomScheme: (controller, url) async { + if (!['intent'].contains(url.scheme)) { await controller.stopLoading(); - try { - await FcmToken().removeToken(Token().getAccessToken()); - await Token().deleteAll(); - isLogin.value = false; - isAuthLogin.value = false; - await _cookieManager.deleteAllCookies(); - await _controller.value!.loadUrl( - urlRequest: URLRequest(url: Uri.parse(address))); - } catch (e) { - // TODO - Fluttertoast.showToast( - msg: "서버와의 연결에 실패했습니다.", - toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); - isAuthLogin.value = false; + if (await canLaunchUrlString(url.toString())) { + await launchUrlString(url.toString(), + mode: LaunchMode.externalApplication); + return; } + switch (url.scheme) { + case 'supertoss': + OpenStore.instance.open( + androidAppBundleId: "viva.republica.toss", + appStoreId: "839333328"); + break; + case 'uber': + OpenStore.instance.open( + androidAppBundleId: "com.ubercab", + appStoreId: "368677368"); + break; + case 'tmoneyonda': + OpenStore.instance.open( + androidAppBundleId: "kr.co.orangetaxi.passenger", + appStoreId: "1489918157"); + break; + case 'kakaotalk': //카카오페이 결제시 + OpenStore.instance.open( + androidAppBundleId: "com.kakao.talk", + appStoreId: "362057947"); + break; + case 'kakaot': + OpenStore.instance.open( + androidAppBundleId: "com.kakao.taxi", + appStoreId: "981110422"); + break; + default: + await Fluttertoast.showToast( + msg: "해당 앱을 실행할 수 없습니다.", + toastLength: Toast.LENGTH_SHORT, + textColor: Colors.black, + backgroundColor: Colors.white); + break; + } + return null; } - }, - onLoadResourceCustomScheme: (controller, url) async { if (Platform.isAndroid) { if (url.scheme == 'intent') { try { @@ -434,8 +720,8 @@ class TaxiView extends HookWidget { await Fluttertoast.showToast( msg: "카카오톡을 실행할 수 없습니다.", toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); } } } @@ -445,14 +731,14 @@ class TaxiView extends HookWidget { // 될 때까지 리로드 if (!isLoaded.value && LoadCount.value < 10) { LoadCount.value++; - } - - if (code == -2) { + } else if (isServerError.value == false && + code != 102 && + code != -999) { Fluttertoast.showToast( msg: "서버와의 연결에 실패했습니다.", toastLength: Toast.LENGTH_SHORT, - textColor: Colors.black, - backgroundColor: Colors.white); + textColor: toastTextColor, + backgroundColor: toastBackgroundColor); isServerError.value = true; } }, @@ -463,92 +749,82 @@ class TaxiView extends HookWidget { }), )), isTimerUp.value && isLoaded.value && isFcmInit.value - ? Stack() + ? const Stack() : Scaffold( body: FadeTransition(opacity: animation, child: loadingView())), isMustUpdate.value ? Container( - color: const Color(0x66C8C8C8), + color: notiColor, child: Center( child: TaxiDialog( - boxContent: { + boxMainContent: { RichText( textAlign: TextAlign.center, text: TextSpan( - text: "새로운 ", - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFF323232), - fontSize: 22, - fontWeight: FontWeight.bold)), + style: Theme.of(context).textTheme.titleSmall, children: const [ - TextSpan(text: "버전"), TextSpan( - text: "이 ", - style: TextStyle(fontWeight: FontWeight.normal)), + text: "새로운 버전", + style: TextStyle( + fontFamily: 'NanumSquare_acB', + fontWeight: FontWeight.w700), + ), + TextSpan(text: "이 "), TextSpan( text: "출시", - style: TextStyle(color: Color(0xFF6E3678))), - TextSpan( - text: "되었습니다!", - style: TextStyle(fontWeight: FontWeight.normal)) + style: TextStyle( + fontFamily: 'NanumSquare_acB', + color: taxiPrimaryColor, + fontWeight: FontWeight.w700)), + TextSpan(text: "되었습니다!") ]), ), + }, + boxSecondaryContent: { Text("정상적인 사용을 위해 앱을 업데이트 해주세요.", textAlign: TextAlign.center, - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFF888888), - fontSize: 12, - fontWeight: FontWeight.bold))), + style: Theme.of(context).textTheme.bodySmall), }, rightButtonContent: "업데이트 하러가기", leftButtonContent: "앱 종료하기", )), ) - : Stack(), + : const Stack(), isServerError.value ? Container( - color: const Color(0x66C8C8C8), + color: notiColor, child: Center( child: TaxiDialog( - boxContent: { + boxMainContent: { RichText( textAlign: TextAlign.center, text: TextSpan( - text: "서버", - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFF323232), - fontSize: 22, - fontWeight: FontWeight.bold)), + style: Theme.of(context).textTheme.titleSmall, children: const [ - TextSpan(text: "와의 "), TextSpan( - text: "연결에 ", - style: TextStyle(fontWeight: FontWeight.normal)), + text: "서버와의 ", + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: "연결에 "), TextSpan( text: "실패", - style: TextStyle(color: Color(0xFF6E3678))), - TextSpan( - text: "했습니다.", - style: TextStyle(fontWeight: FontWeight.normal)) + style: TextStyle( + color: taxiPrimaryColor, + fontWeight: FontWeight.bold)), + TextSpan(text: "했습니다.") ]), ), - Padding(padding: EdgeInsets.only(top: 5)), + }, + boxSecondaryContent: { Text("일시적인 오류일 수 있습니다.", textAlign: TextAlign.center, - style: GoogleFonts.roboto( - textStyle: const TextStyle( - color: Color(0xFF888888), - fontSize: 12, - fontWeight: FontWeight.bold))), + style: Theme.of(context).textTheme.bodySmall), }, rightButtonContent: "스토어로 가기", leftButtonContent: "앱 종료하기", )), ) - : Stack() + : const Stack() ])); } @@ -576,8 +852,8 @@ class TaxiView extends HookWidget { backCount.value = true; Fluttertoast.showToast( msg: "한번 더 누르시면 앱을 종료합니다.", - backgroundColor: Colors.white, - textColor: Colors.black, + backgroundColor: toastBackgroundColor, + textColor: toastTextColor, toastLength: Toast.LENGTH_SHORT, ); return false; diff --git a/pubspec.yaml b/pubspec.yaml index 2bccdc3..7935def 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.2+3 +version: 1.0.4+29 environment: sdk: ">=2.17.5 <3.0.0" @@ -53,6 +53,10 @@ dependencies: firebase_dynamic_links: ^5.1.1 app_links: ^3.4.3 firebase_crashlytics: ^3.3.1 + url_launcher: ^6.1.14 + social_share: ^2.3.1 + path_provider: ^2.1.1 + http: ^1.1.0 dev_dependencies: flutter_test: