From 20685436ed1cebc9810f300dea3dc361c416af4c Mon Sep 17 00:00:00 2001 From: taetae98coding Date: Mon, 11 Nov 2024 03:00:12 +0900 Subject: [PATCH] feat: server - FCM refactor: app - compose feat: app - FCM --- Diary/Diary.xcodeproj/project.pbxproj | 73 ++++++++++------- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../xcschemes/DevRelease.xcscheme | 78 +++++++++++++++++++ Diary/Diary/AppDelegate.swift | 47 +++++++++-- Diary/Diary/Diary.entitlements | 8 ++ Diary/Diary/Info.plist | 5 ++ .../diary/core/coroutines/CoroutinesModule.kt | 7 ++ .../database/memory/MemoBackupMemoryDao.kt | 25 ------ .../database/room/dao/MemoBackupRoomDao.kt | 20 ----- .../core/diary/database/MemoBackupDao.kt | 2 - .../core/diary/service/fcm/FCMService.kt | 33 ++++++++ app/data/account/build.gradle.kts | 1 - .../repository/AccountRepositoryImpl.kt | 29 ------- .../backup/repository/BackupRepositoryImpl.kt | 15 +--- app/data/credential/build.gradle.kts | 15 ++++ .../data/credential/CredentialDataModule.kt | 8 ++ .../repository/CredentialRepositoryImpl.kt | 31 ++++++++ app/data/fcm/build.gradle.kts | 15 ++++ .../diary/data/fcm/FCMDataModule.kt | 8 ++ .../data/fcm/repository/FCMRepositoryImpl.kt | 20 +++++ .../memo/repository/MemoRepositoryImpl.kt | 20 +---- .../account/repository/AccountRepository.kt | 6 -- .../domain/account/usecase/LoginUseCase.kt | 18 ----- .../domain/account/usecase/LogoutUseCase.kt | 13 ---- .../backup/repository/BackupRepository.kt | 8 +- .../domain/backup/usecase/BackupUseCase.kt | 27 ++----- app/domain/credential/build.gradle.kts | 15 ++++ .../credential/CredentialDomainModule.kt | 8 ++ .../repository/CredentialRepository.kt | 12 +++ .../domain/credential}/usecase/JoinUseCase.kt | 6 +- .../domain/credential/usecase/LoginUseCase.kt | 33 ++++++++ .../credential/usecase/LogoutUseCase.kt | 21 +++++ app/domain/fcm/build.gradle.kts | 13 ++++ .../diary/domain/fcm/FCMDomainModule.kt | 8 ++ .../domain/fcm/repository/FCMRepository.kt | 6 ++ .../fcm/usecase/UpdateFCMTokenUseCase.kt | 24 ++++++ .../domain/fetch/usecase/FetchUseCase.kt | 19 ++--- app/domain/memo/build.gradle.kts | 1 + .../domain/memo/repository/MemoRepository.kt | 8 +- .../domain/memo/usecase/AddMemoUseCase.kt | 17 +++- .../domain/memo/usecase/DeleteMemoUseCase.kt | 16 +++- .../domain/memo/usecase/FinishMemoUseCase.kt | 16 +++- .../domain/memo/usecase/RestartMemoUseCase.kt | 17 +++- .../domain/memo/usecase/UpdateMemoUseCase.kt | 18 ++++- app/feature/account/build.gradle.kts | 1 + .../feature/account/join/JoinViewModel.kt | 4 +- .../feature/account/login/LoginViewModel.kt | 2 +- app/feature/more/build.gradle.kts | 1 + .../more/viewmodel/MoreAccountViewModel.kt | 2 +- app/platform/android/build.gradle.kts | 7 ++ .../android/src/main/AndroidManifest.xml | 24 +++++- .../taetae98coding/diary/DiaryActivity.kt | 9 +++ .../taetae98coding/diary/DiaryApplication.kt | 26 +------ .../taetae98coding/diary/KoinAndroidModule.kt | 8 ++ .../initializer/BackupManagerInitializer.kt | 23 ++++++ .../initializer/FCMManagerInitializer.kt | 23 ++++++ .../initializer/FetchManagerInitializer.kt | 23 ++++++ .../diary/initializer/KoinInitializer.kt | 2 + .../DefaultNotificationManager.kt | 41 ++++++++++ .../service/DiaryFirebaseMessagingService.kt | 45 +++++++++++ .../res/drawable/ic_android_black_24dp.xml | 5 ++ .../src/main/res/values-ko/strings.xml | 5 ++ .../android/src/main/res/values/strings.xml | 5 ++ app/platform/common/build.gradle.kts | 5 ++ .../taetae98coding/diary/app/AppModule.kt | 16 ++++ .../diary/app/manager/FCMManager.kt | 22 ++++++ .../InitFirebaseMessagingManaver.kt | 18 +++++ .../diary/initializer/IosInitializer.kt | 1 + build.gradle.kts | 1 + .../model/request/fcm/DeleteFCMRequest.kt | 10 +++ .../model/request/fcm/UpsertFCMRequest.kt | 10 +++ gradle/libs.versions.toml | 8 +- library/firebase-common/build.gradle.kts | 8 ++ .../diary/library/firebase/KFirebase.kt | 3 + library/firebase-messaging/build.gradle.kts | 48 ++++++++++++ .../KFirebaseMessagingExt.android.kt | 6 ++ .../messaging/KFirebaseMessagingImpl.kt | 11 +++ .../firebase/messaging/KFirebaseMessaging.kt | 5 ++ .../messaging/KFirebaseMessagingExt.kt | 5 ++ .../messaging/KFirebaseMessagingExt.ios.kt | 6 ++ .../messaging/KFirebaseMessagingImpl.kt | 24 ++++++ .../KFirebaseMessagingExt.nonSupport.kt | 6 ++ .../messaging/KFirebaseMessagingImpl.kt | 7 ++ server/app/build.gradle.kts | 3 + .../diary/plugin/DatabasePlugin.kt | 21 ++--- .../taetae98coding/diary/plugin/KoinPlugin.kt | 20 +++-- .../diary/plugin/RoutingPlugin.kt | 2 + .../diary/core/database/FCMTokenTable.kt | 40 ++++++++++ .../repository/AccountRepositoryImpl.kt | 2 +- server/data/fcm/build.gradle.kts | 11 +++ .../diary/data/fcm/FCMDataModule.kt | 8 ++ .../data/fcm/repository/FCMRepositoryImpl.kt | 16 ++++ server/domain/fcm/build.gradle.kts | 3 + .../diary/domain/fcm/FCMDomainModule.kt | 8 ++ .../domain/fcm/repository/FCMRepository.kt | 6 ++ .../fcm/usecase/DeleteFCMTokenUseCase.kt | 13 ++++ .../fcm/usecase/UpsertFCMTokenUseCase.kt | 13 ++++ server/feature/fcm/build.gradle.kts | 7 ++ .../diary/feature/fcm/FCMRouting.kt | 46 +++++++++++ .../diary/feature/memo/MemoRouting.kt | 9 --- settings.gradle.kts | 10 +++ 101 files changed, 1213 insertions(+), 295 deletions(-) create mode 100644 Diary/Diary.xcodeproj/xcshareddata/xcschemes/DevRelease.xcscheme create mode 100644 Diary/Diary/Diary.entitlements create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/fcm/FCMService.kt create mode 100644 app/data/credential/build.gradle.kts create mode 100644 app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/CredentialDataModule.kt create mode 100644 app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/repository/CredentialRepositoryImpl.kt create mode 100644 app/data/fcm/build.gradle.kts create mode 100644 app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt create mode 100644 app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt delete mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt delete mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt create mode 100644 app/domain/credential/build.gradle.kts create mode 100644 app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/CredentialDomainModule.kt create mode 100644 app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/repository/CredentialRepository.kt rename app/domain/{account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account => credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential}/usecase/JoinUseCase.kt (71%) create mode 100644 app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LoginUseCase.kt create mode 100644 app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LogoutUseCase.kt create mode 100644 app/domain/fcm/build.gradle.kts create mode 100644 app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt create mode 100644 app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt create mode 100644 app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpdateFCMTokenUseCase.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/KoinAndroidModule.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FCMManagerInitializer.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/notification/DefaultNotificationManager.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/service/DiaryFirebaseMessagingService.kt create mode 100644 app/platform/android/src/main/res/drawable/ic_android_black_24dp.xml create mode 100644 app/platform/android/src/main/res/values-ko/strings.xml create mode 100644 app/platform/android/src/main/res/values/strings.xml create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FCMManager.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/InitFirebaseMessagingManaver.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/DeleteFCMRequest.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/UpsertFCMRequest.kt create mode 100644 library/firebase-common/build.gradle.kts create mode 100644 library/firebase-common/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/KFirebase.kt create mode 100644 library/firebase-messaging/build.gradle.kts create mode 100644 library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.android.kt create mode 100644 library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt create mode 100644 library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessaging.kt create mode 100644 library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.kt create mode 100644 library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt create mode 100644 library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt create mode 100644 library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.nonSupport.kt create mode 100644 library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt create mode 100644 server/data/fcm/build.gradle.kts create mode 100644 server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt create mode 100644 server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt create mode 100644 server/domain/fcm/build.gradle.kts create mode 100644 server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt create mode 100644 server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt create mode 100644 server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/DeleteFCMTokenUseCase.kt create mode 100644 server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpsertFCMTokenUseCase.kt create mode 100644 server/feature/fcm/build.gradle.kts create mode 100644 server/feature/fcm/src/main/kotlin/io/github/taetae98coding/diary/feature/fcm/FCMRouting.kt diff --git a/Diary/Diary.xcodeproj/project.pbxproj b/Diary/Diary.xcodeproj/project.pbxproj index 8062eee1..d0957aba 100644 --- a/Diary/Diary.xcodeproj/project.pbxproj +++ b/Diary/Diary.xcodeproj/project.pbxproj @@ -7,9 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 3DD9A74F2CDFBAC60023C4EE /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 3DD9A74E2CDFBAC60023C4EE /* FirebaseAnalyticsWithoutAdIdSupport */; }; - 3DD9A7512CDFBAC60023C4EE /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3DD9A7502CDFBAC60023C4EE /* FirebaseCrashlytics */; }; - 3DD9A7532CDFBAC60023C4EE /* FirebasePerformance in Frameworks */ = {isa = PBXBuildFile; productRef = 3DD9A7522CDFBAC60023C4EE /* FirebasePerformance */; }; + 3DF0CB1B2CE4D18600B88439 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3DF0CB1A2CE4D18600B88439 /* FirebaseAnalytics */; }; + 3DF0CB1D2CE4D18600B88439 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3DF0CB1C2CE4D18600B88439 /* FirebaseCrashlytics */; }; + 3DF0CB1F2CE4D18600B88439 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 3DF0CB1E2CE4D18600B88439 /* FirebaseMessaging */; }; + 3DF0CB212CE4D18600B88439 /* FirebasePerformance in Frameworks */ = {isa = PBXBuildFile; productRef = 3DF0CB202CE4D18600B88439 /* FirebasePerformance */; }; + 3DF0CB232CE4D18600B88439 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 3DF0CB222CE4D18600B88439 /* FirebaseRemoteConfig */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -42,9 +44,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3DD9A74F2CDFBAC60023C4EE /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */, - 3DD9A7512CDFBAC60023C4EE /* FirebaseCrashlytics in Frameworks */, - 3DD9A7532CDFBAC60023C4EE /* FirebasePerformance in Frameworks */, + 3DF0CB1F2CE4D18600B88439 /* FirebaseMessaging in Frameworks */, + 3DF0CB1B2CE4D18600B88439 /* FirebaseAnalytics in Frameworks */, + 3DF0CB212CE4D18600B88439 /* FirebasePerformance in Frameworks */, + 3DF0CB1D2CE4D18600B88439 /* FirebaseCrashlytics in Frameworks */, + 3DF0CB232CE4D18600B88439 /* FirebaseRemoteConfig in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,9 +94,11 @@ ); name = Diary; packageProductDependencies = ( - 3DD9A74E2CDFBAC60023C4EE /* FirebaseAnalyticsWithoutAdIdSupport */, - 3DD9A7502CDFBAC60023C4EE /* FirebaseCrashlytics */, - 3DD9A7522CDFBAC60023C4EE /* FirebasePerformance */, + 3DF0CB1A2CE4D18600B88439 /* FirebaseAnalytics */, + 3DF0CB1C2CE4D18600B88439 /* FirebaseCrashlytics */, + 3DF0CB1E2CE4D18600B88439 /* FirebaseMessaging */, + 3DF0CB202CE4D18600B88439 /* FirebasePerformance */, + 3DF0CB222CE4D18600B88439 /* FirebaseRemoteConfig */, ); productName = Diary; productReference = 3DDC71F62CCD5903001193A2 /* Diary.app */; @@ -124,7 +130,7 @@ mainGroup = 3DDC71ED2CCD5903001193A2; minimizedProjectReferenceProxies = 1; packageReferences = ( - 3DD9A74D2CDFBAC60023C4EE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); preferredProjectObjectVersion = 77; productRefGroup = 3DDC71F72CCD5903001193A2 /* Products */; @@ -286,6 +292,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; @@ -294,7 +301,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Diary/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Diary-RealDebug"; + INFOPLIST_KEY_CFBundleDisplayName = "Diary-DevDebug"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -376,18 +383,17 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4TV6L66XZ8; + DEVELOPMENT_TEAM = 4TV6L66XZ8; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Diary/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Diary; + INFOPLIST_KEY_CFBundleDisplayName = "Diary-DevDebug"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -403,7 +409,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = adhoc; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -478,6 +483,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; @@ -568,6 +574,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Diary/Diary.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; @@ -576,7 +584,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Diary/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "Diary-DevRelease"; + INFOPLIST_KEY_CFBundleDisplayName = "Diary-DevDebug"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -591,6 +599,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -625,32 +634,42 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 3DD9A74D2CDFBAC60023C4EE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; requirement = { kind = exactVersion; - version = 11.4.0; + version = 11.5.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 3DD9A74E2CDFBAC60023C4EE /* FirebaseAnalyticsWithoutAdIdSupport */ = { + 3DF0CB1A2CE4D18600B88439 /* FirebaseAnalytics */ = { isa = XCSwiftPackageProductDependency; - package = 3DD9A74D2CDFBAC60023C4EE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseAnalyticsWithoutAdIdSupport; + package = 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalytics; }; - 3DD9A7502CDFBAC60023C4EE /* FirebaseCrashlytics */ = { + 3DF0CB1C2CE4D18600B88439 /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; - package = 3DD9A74D2CDFBAC60023C4EE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + package = 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseCrashlytics; }; - 3DD9A7522CDFBAC60023C4EE /* FirebasePerformance */ = { + 3DF0CB1E2CE4D18600B88439 /* FirebaseMessaging */ = { + isa = XCSwiftPackageProductDependency; + package = 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseMessaging; + }; + 3DF0CB202CE4D18600B88439 /* FirebasePerformance */ = { isa = XCSwiftPackageProductDependency; - package = 3DD9A74D2CDFBAC60023C4EE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + package = 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebasePerformance; }; + 3DF0CB222CE4D18600B88439 /* FirebaseRemoteConfig */ = { + isa = XCSwiftPackageProductDependency; + package = 3DF0CB192CE4D18600B88439 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseRemoteConfig; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 3DDC71EE2CCD5903001193A2 /* Project object */; diff --git a/Diary/Diary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Diary/Diary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 91a89a13..78194563 100644 --- a/Diary/Diary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Diary/Diary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8328630971a8fdd8072b36bb22bef732eb15e1f0", - "version" : "11.4.0" + "revision" : "dbdfdc44bee8b8e4eaa5ec27eb12b9338f3f2bc1", + "version" : "11.5.0" } }, { diff --git a/Diary/Diary.xcodeproj/xcshareddata/xcschemes/DevRelease.xcscheme b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/DevRelease.xcscheme new file mode 100644 index 00000000..acc102d5 --- /dev/null +++ b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/DevRelease.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Diary/Diary/AppDelegate.swift b/Diary/Diary/AppDelegate.swift index a4e1a554..dc07bb18 100644 --- a/Diary/Diary/AppDelegate.swift +++ b/Diary/Diary/AppDelegate.swift @@ -1,12 +1,45 @@ import SwiftUI -import FirebaseCore - +import Firebase +import UserNotifications class AppDelegate: NSObject, UIApplicationDelegate { - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - FirebaseApp.configure() + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + FirebaseApp.configure() + + initRemoteNotifications(application: application) + initFirebaseMessaging() + + return true + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + Messaging.messaging().apnsToken = deviceToken + } + + + func initRemoteNotifications(application: UIApplication) { + UNUserNotificationCenter.current().delegate = self + + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: { _, _ in } + ) + + application.registerForRemoteNotifications() + } + + func initFirebaseMessaging() { + Messaging.messaging().delegate = self + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + +} - return true - } +extension AppDelegate: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + print("Firebase registration token: \(String(describing: fcmToken))") + } } diff --git a/Diary/Diary/Diary.entitlements b/Diary/Diary/Diary.entitlements new file mode 100644 index 00000000..903def2a --- /dev/null +++ b/Diary/Diary/Diary.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/Diary/Diary/Info.plist b/Diary/Diary/Info.plist index ba6b757e..6d3967f7 100644 --- a/Diary/Diary/Info.plist +++ b/Diary/Diary/Info.plist @@ -9,5 +9,10 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + fetch + remote-notification + diff --git a/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt index f34ab89e..747086b1 100644 --- a/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt +++ b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt @@ -1,6 +1,8 @@ package io.github.taetae98coding.diary.core.coroutines import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineScope import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module import org.koin.core.annotation.Singleton @@ -12,4 +14,9 @@ public class CoroutinesModule { internal fun providesAppLifecycleOwner(): LifecycleOwner { return getAppLifecycleOwner() } + + @Singleton + internal fun providesAppCoroutineScope(lifecycleOwner: LifecycleOwner): CoroutineScope { + return lifecycleOwner.lifecycleScope + } } diff --git a/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt index 2ec338c9..b9757a62 100644 --- a/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt +++ b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt @@ -5,9 +5,7 @@ import io.github.taetae98coding.diary.core.model.memo.MemoDto import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.update import org.koin.core.annotation.Singleton @OptIn(ExperimentalCoroutinesApi::class) @@ -16,7 +14,6 @@ internal class MemoBackupMemoryDao( private val memoMemoryDao: MemoMemoryDao, ) : MemoBackupDao { private val flow = MutableStateFlow>>(emptyMap()) - private val updateFlow = mutableMapOf>() override suspend fun upsert(uid: String, memoId: String) { val set = buildSet { @@ -28,20 +25,6 @@ internal class MemoBackupMemoryDao( put(uid, set) } - flow.emit(map) - getInternalUpdateFlow(uid).update { it + 1 } - } - - override suspend fun delete(uid: String, memoId: String) { - val set = buildSet { - flow.value[uid]?.let { addAll(it) } - remove(memoId) - } - val map = buildMap { - putAll(flow.value) - put(uid, set) - } - flow.emit(map) } @@ -59,10 +42,6 @@ internal class MemoBackupMemoryDao( flow.emit(map) } - override fun getUpdateFlow(uid: String): Flow { - return getInternalUpdateFlow(uid).asStateFlow() - } - override fun countByUid(uid: String): Flow { return flow.mapLatest { it[uid]?.size ?: 0 } } @@ -72,8 +51,4 @@ internal class MemoBackupMemoryDao( map.values.filter { it.owner == uid } } } - - private fun getInternalUpdateFlow(uid: String): MutableStateFlow { - return updateFlow.getOrPut(uid) { MutableStateFlow(0) } - } } diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt index 6d198b24..a196431e 100644 --- a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt @@ -8,9 +8,6 @@ import io.github.taetae98coding.diary.core.diary.database.room.mapper.toDto import io.github.taetae98coding.diary.core.model.memo.MemoDto import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import org.koin.core.annotation.Factory @Factory @@ -19,21 +16,12 @@ internal class MemoBackupRoomDao( ) : MemoBackupDao { override suspend fun upsert(uid: String, memoId: String) { database.memoBackup().upsert(MemoBackupEntity(uid, memoId)) - getInternalUpdateFlow(uid).update { it + 1 } - } - - override suspend fun delete(uid: String, memoId: String) { - database.memoBackup().delete(MemoBackupEntity(uid, memoId)) } override suspend fun deleteByMemoIds(memoIds: List) { database.memoBackup().deleteByMemoIds(memoIds) } - override fun getUpdateFlow(uid: String): Flow { - return getInternalUpdateFlow(uid).asStateFlow() - } - override fun countByUid(uid: String): Flow { return database.memoBackup().countByUid(uid) } @@ -42,12 +30,4 @@ internal class MemoBackupRoomDao( return database.memoBackup().findByUid(uid) .mapCollectionLatest(MemoEntity::toDto) } - - companion object { - private val updateFlow = mutableMapOf>() - - private fun getInternalUpdateFlow(uid: String): MutableStateFlow { - return updateFlow.getOrPut(uid) { MutableStateFlow(0) } - } - } } diff --git a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt index 6d21188b..a5d3cd34 100644 --- a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt @@ -5,10 +5,8 @@ import kotlinx.coroutines.flow.Flow public interface MemoBackupDao { public suspend fun upsert(uid: String, memoId: String) - public suspend fun delete(uid: String, memoId: String) public suspend fun deleteByMemoIds(memoIds: List) - public fun getUpdateFlow(uid: String): Flow public fun countByUid(uid: String): Flow public fun findByUid(uid: String): Flow> } diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/fcm/FCMService.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/fcm/FCMService.kt new file mode 100644 index 00000000..c7010ad9 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/fcm/FCMService.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.core.diary.service.fcm + +import io.github.taetae98coding.diary.common.model.request.fcm.DeleteFCMRequest +import io.github.taetae98coding.diary.common.model.request.fcm.UpsertFCMRequest +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.diary.service.ext.getOrThrow +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named + +@Factory +public class FCMService internal constructor( + @Named(DiaryServiceModule.DIARY_CLIENT) + private val client: HttpClient, +) { + public suspend fun upsert(token: String) { + return client.post("/fcm/upsert") { + contentType(ContentType.Application.Json) + setBody(UpsertFCMRequest(token)) + }.getOrThrow() + } + + public suspend fun delete(token: String) { + return client.post("/fcm/delete") { + contentType(ContentType.Application.Json) + setBody(DeleteFCMRequest(token)) + }.getOrThrow() + } +} diff --git a/app/data/account/build.gradle.kts b/app/data/account/build.gradle.kts index 884c43b4..63a9b8d0 100644 --- a/app/data/account/build.gradle.kts +++ b/app/data/account/build.gradle.kts @@ -7,7 +7,6 @@ kotlin { commonMain { dependencies { implementation(project(":app:core:account-preferences")) - implementation(project(":app:core:diary-service")) implementation(project(":app:domain:account")) } } diff --git a/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt index 3f8f7ebe..4443515d 100644 --- a/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt +++ b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt @@ -1,33 +1,14 @@ package io.github.taetae98coding.diary.data.account.repository import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences -import io.github.taetae98coding.diary.core.diary.service.account.AccountService -import io.github.taetae98coding.diary.core.model.account.AccountToken import io.github.taetae98coding.diary.domain.account.repository.AccountRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.mapLatest import org.koin.core.annotation.Factory -@OptIn(ExperimentalCoroutinesApi::class) @Factory internal class AccountRepositoryImpl( private val preferencesDataSource: AccountPreferences, - private val remoteDataSource: AccountService, ) : AccountRepository { - override suspend fun join(email: String, password: String) { - remoteDataSource.join(email, password) - } - - override suspend fun save(email: String, token: AccountToken) { - preferencesDataSource.save(email, token.uid, token.token) - } - - override suspend fun clear() { - preferencesDataSource.clear() - } - override fun getEmail(): Flow { return preferencesDataSource.getEmail() } @@ -35,14 +16,4 @@ internal class AccountRepositoryImpl( override fun getUid(): Flow { return preferencesDataSource.getUid() } - - override fun fetchToken(email: String, password: String): Flow { - return flow { emit(remoteDataSource.login(email, password)) } - .mapLatest { - AccountToken( - uid = it.uid, - token = it.token, - ) - } - } } diff --git a/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt index 1482317a..0e6cb91b 100644 --- a/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt +++ b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt @@ -5,19 +5,15 @@ import io.github.taetae98coding.diary.core.diary.service.memo.MemoService import io.github.taetae98coding.diary.core.model.mapper.toMemo import io.github.taetae98coding.diary.core.model.memo.MemoDto import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.mapLatest import org.koin.core.annotation.Factory -@OptIn(ExperimentalCoroutinesApi::class) @Factory internal class BackupRepositoryImpl( private val memoBackupDao: MemoBackupDao, private val memoService: MemoService, ) : BackupRepository { - override suspend fun backupMemo(uid: String) { + override suspend fun backup(uid: String) { while (memoBackupDao.countByUid(uid).first() > 0) { val memoList = memoBackupDao.findByUid(uid).first() .map(MemoDto::toMemo) @@ -27,12 +23,7 @@ internal class BackupRepositoryImpl( } } - override fun getUpdateFlow(uid: String): Flow { - return memoBackupDao.getUpdateFlow(uid) - .mapLatest { } - } - - override fun countBackupMemo(uid: String): Flow { - return memoBackupDao.countByUid(uid) + override suspend fun upsertMemoBackupQueue(uid: String, memoId: String) { + memoBackupDao.upsert(uid, memoId) } } diff --git a/app/data/credential/build.gradle.kts b/app/data/credential/build.gradle.kts new file mode 100644 index 00000000..87aa7143 --- /dev/null +++ b/app/data/credential/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:account-preferences")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:domain:credential")) + } + } + } +} diff --git a/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/CredentialDataModule.kt b/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/CredentialDataModule.kt new file mode 100644 index 00000000..0eb57a39 --- /dev/null +++ b/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/CredentialDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.credential + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class CredentialDataModule diff --git a/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/repository/CredentialRepositoryImpl.kt b/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/repository/CredentialRepositoryImpl.kt new file mode 100644 index 00000000..05c6e764 --- /dev/null +++ b/app/data/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/data/credential/repository/CredentialRepositoryImpl.kt @@ -0,0 +1,31 @@ +package io.github.taetae98coding.diary.data.credential.repository + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import io.github.taetae98coding.diary.core.diary.service.account.AccountService +import io.github.taetae98coding.diary.core.model.account.AccountToken +import io.github.taetae98coding.diary.domain.credential.repository.CredentialRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.koin.core.annotation.Factory + +@Factory +internal class CredentialRepositoryImpl( + private val preferencesDataSource: AccountPreferences, + private val remoteDataSource: AccountService, +) : CredentialRepository { + override suspend fun join(email: String, password: String) { + remoteDataSource.join(email, password) + } + + override suspend fun save(email: String, token: AccountToken) { + preferencesDataSource.save(email, token.uid, token.token) + } + + override suspend fun clear() { + preferencesDataSource.clear() + } + + override fun fetchToken(email: String, password: String): Flow { + return flow { emit(remoteDataSource.login(email, password)) } + } +} diff --git a/app/data/fcm/build.gradle.kts b/app/data/fcm/build.gradle.kts new file mode 100644 index 00000000..4e67d865 --- /dev/null +++ b/app/data/fcm/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-service")) + implementation(project(":app:domain:fcm")) + implementation(project(":library:firebase-messaging")) + } + } + } +} diff --git a/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt b/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt new file mode 100644 index 00000000..e9adca4d --- /dev/null +++ b/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.fcm + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FCMDataModule diff --git a/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt b/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt new file mode 100644 index 00000000..db123e5c --- /dev/null +++ b/app/data/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.data.fcm.repository + +import io.github.taetae98coding.diary.core.diary.service.fcm.FCMService +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import io.github.taetae98coding.diary.library.firebase.messaging.KFirebaseMessaging +import org.koin.core.annotation.Factory + +@Factory +internal class FCMRepositoryImpl( + private val messaging: KFirebaseMessaging, + private val remoteDataSource: FCMService, +) : FCMRepository { + override suspend fun upsert() { + remoteDataSource.upsert(messaging.getToken()) + } + + override suspend fun delete() { + remoteDataSource.delete(messaging.getToken()) + } +} diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt index e72bf0fa..334c4141 100644 --- a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -20,7 +20,7 @@ internal class MemoRepositoryImpl( private val localDataSource: MemoDao, private val backupDataSource: MemoBackupDao, ) : MemoRepository { - override suspend fun upsert(uid: String?, memo: Memo) { + override suspend fun upsert(memo: Memo) { val dto = MemoDto( id = memo.id, detail = memo.detail, @@ -32,30 +32,18 @@ internal class MemoRepositoryImpl( ) localDataSource.upsert(dto) - if (!uid.isNullOrBlank()) { - backupDataSource.upsert(uid, memo.id) - } } - override suspend fun update(uid: String?, memoId: String, detail: MemoDetail) { + override suspend fun update(memoId: String, detail: MemoDetail) { localDataSource.update(memoId, detail) - if (!uid.isNullOrBlank()) { - backupDataSource.upsert(uid, memoId) - } } - override suspend fun updateFinish(uid: String?, memoId: String, isFinish: Boolean) { + override suspend fun updateFinish(memoId: String, isFinish: Boolean) { localDataSource.updateFinish(memoId, isFinish) - if (!uid.isNullOrBlank()) { - backupDataSource.upsert(uid, memoId) - } } - override suspend fun updateDelete(uid: String?, memoId: String, isDelete: Boolean) { + override suspend fun updateDelete(memoId: String, isDelete: Boolean) { localDataSource.updateDelete(memoId, isDelete) - if (!uid.isNullOrBlank()) { - backupDataSource.upsert(uid, memoId) - } } override fun find(memoId: String): Flow { diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt index 63bb8ea0..d5b02b31 100644 --- a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt @@ -1,14 +1,8 @@ package io.github.taetae98coding.diary.domain.account.repository -import io.github.taetae98coding.diary.core.model.account.AccountToken import kotlinx.coroutines.flow.Flow public interface AccountRepository { - public suspend fun join(email: String, password: String) - public suspend fun save(email: String, token: AccountToken) - public suspend fun clear() - - public fun fetchToken(email: String, password: String): Flow public fun getEmail(): Flow public fun getUid(): Flow } diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt deleted file mode 100644 index c7fd3ca7..00000000 --- a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.taetae98coding.diary.domain.account.usecase - -import io.github.taetae98coding.diary.domain.account.repository.AccountRepository -import kotlinx.coroutines.flow.first -import org.koin.core.annotation.Factory - -@Factory -public class LoginUseCase internal constructor( - private val repository: AccountRepository, -) { - public suspend operator fun invoke(email: String, password: String): Result { - return runCatching { - val token = repository.fetchToken(email, password).first() - - repository.save(email = email, token = token) - } - } -} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt deleted file mode 100644 index 75fe3e98..00000000 --- a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.taetae98coding.diary.domain.account.usecase - -import io.github.taetae98coding.diary.domain.account.repository.AccountRepository -import org.koin.core.annotation.Factory - -@Factory -public class LogoutUseCase internal constructor( - private val repository: AccountRepository, -) { - public suspend operator fun invoke(): Result { - return runCatching { repository.clear() } - } -} diff --git a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt index 38f5474a..1bdd1c52 100644 --- a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt +++ b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt @@ -1,10 +1,6 @@ package io.github.taetae98coding.diary.domain.backup.repository -import kotlinx.coroutines.flow.Flow - public interface BackupRepository { - public suspend fun backupMemo(uid: String) - - public fun getUpdateFlow(uid: String): Flow - public fun countBackupMemo(uid: String): Flow + public suspend fun backup(uid: String) + public suspend fun upsertMemoBackupQueue(uid: String, memoId: String) } diff --git a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt index 792c6244..3b18bedb 100644 --- a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt +++ b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt @@ -3,14 +3,9 @@ package io.github.taetae98coding.diary.domain.backup.usecase import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory -@OptIn(ExperimentalCoroutinesApi::class) @Factory public class BackupUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, @@ -18,22 +13,10 @@ public class BackupUseCase internal constructor( ) { public suspend operator fun invoke(): Result { return runCatching { - getAccountUseCase().mapLatest { it.getOrNull() } - .flatMapLatest { account -> - if (account is Account.Member) { - repository.getUpdateFlow(account.uid) - .mapLatest { account.uid } - } else { - emptyFlow() - } - } - .collectLatest { uid -> - runCatching { backup(uid) } - } + val account = getAccountUseCase().first().getOrThrow() + if (account is Account.Member) { + repository.backup(account.uid) + } } } - - private suspend fun backup(uid: String) { - repository.backupMemo(uid) - } } diff --git a/app/domain/credential/build.gradle.kts b/app/domain/credential/build.gradle.kts new file mode 100644 index 00000000..075adf43 --- /dev/null +++ b/app/domain/credential/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:fetch")) + implementation(project(":app:domain:backup")) + implementation(project(":app:domain:fcm")) + } + } + } +} diff --git a/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/CredentialDomainModule.kt b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/CredentialDomainModule.kt new file mode 100644 index 00000000..76e4c4e7 --- /dev/null +++ b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/CredentialDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.credential + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class CredentialDomainModule diff --git a/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/repository/CredentialRepository.kt b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/repository/CredentialRepository.kt new file mode 100644 index 00000000..8bfddcbe --- /dev/null +++ b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/repository/CredentialRepository.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.domain.credential.repository + +import io.github.taetae98coding.diary.core.model.account.AccountToken +import kotlinx.coroutines.flow.Flow + +public interface CredentialRepository { + public suspend fun join(email: String, password: String) + public suspend fun save(email: String, token: AccountToken) + public suspend fun clear() + + public fun fetchToken(email: String, password: String): Flow +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/JoinUseCase.kt similarity index 71% rename from app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt rename to app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/JoinUseCase.kt index d8e2acc2..2fa964aa 100644 --- a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt +++ b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/JoinUseCase.kt @@ -1,13 +1,13 @@ -package io.github.taetae98coding.diary.domain.account.usecase +package io.github.taetae98coding.diary.domain.credential.usecase import io.github.taetae98coding.diary.common.exception.account.InvalidEmailException -import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.domain.credential.repository.CredentialRepository import io.github.taetae98coding.diary.library.kotlin.regex.email import org.koin.core.annotation.Factory @Factory public class JoinUseCase internal constructor( - private val repository: AccountRepository, + private val repository: CredentialRepository, ) { public suspend operator fun invoke(email: String, password: String): Result { return runCatching { diff --git a/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LoginUseCase.kt b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LoginUseCase.kt new file mode 100644 index 00000000..a09a93f9 --- /dev/null +++ b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LoginUseCase.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.domain.credential.usecase + +import io.github.taetae98coding.diary.domain.credential.repository.CredentialRepository +import io.github.taetae98coding.diary.domain.fcm.usecase.UpdateFCMTokenUseCase +import io.github.taetae98coding.diary.domain.fetch.usecase.FetchUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +@Factory +public class LoginUseCase internal constructor( + private val coroutineScope: CoroutineScope, + private val repository: CredentialRepository, + private val fetchUseCase: FetchUseCase, + private val updateFCMTokenUseCase: UpdateFCMTokenUseCase, +) { + public suspend operator fun invoke(email: String, password: String): Result { + return runCatching { + val token = repository.fetchToken(email, password).first() + repository.save(email = email, token = token) + + coroutineScope.launch { + listOf( + async { fetchUseCase() }, + async { updateFCMTokenUseCase() } + ).awaitAll() + } + } + } +} diff --git a/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LogoutUseCase.kt b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..5ab2f81e --- /dev/null +++ b/app/domain/credential/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/credential/usecase/LogoutUseCase.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.domain.credential.usecase + +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase +import io.github.taetae98coding.diary.domain.credential.repository.CredentialRepository +import io.github.taetae98coding.diary.domain.fcm.usecase.UpdateFCMTokenUseCase +import org.koin.core.annotation.Factory + +@Factory +public class LogoutUseCase internal constructor( + private val repository: CredentialRepository, + private val backupUseCase: BackupUseCase, + private val updateFCMTokenUseCase: UpdateFCMTokenUseCase, +) { + public suspend operator fun invoke(): Result { + return runCatching { + backupUseCase() + repository.clear() + updateFCMTokenUseCase() + } + } +} diff --git a/app/domain/fcm/build.gradle.kts b/app/domain/fcm/build.gradle.kts new file mode 100644 index 00000000..84e3b1c2 --- /dev/null +++ b/app/domain/fcm/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} diff --git a/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt new file mode 100644 index 00000000..a2d83583 --- /dev/null +++ b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.fcm + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FCMDomainModule diff --git a/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt new file mode 100644 index 00000000..76699848 --- /dev/null +++ b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.domain.fcm.repository + +public interface FCMRepository { + public suspend fun upsert() + public suspend fun delete() +} diff --git a/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpdateFCMTokenUseCase.kt b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpdateFCMTokenUseCase.kt new file mode 100644 index 00000000..ddb14d82 --- /dev/null +++ b/app/domain/fcm/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpdateFCMTokenUseCase.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.domain.fcm.usecase + +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class UpdateFCMTokenUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: FCMRepository, +) { + public suspend operator fun invoke(): Result { + return runCatching { + val account = getAccountUseCase().first().getOrThrow() + if (account is Account.Member) { + repository.upsert() + } else { + repository.delete() + } + } + } +} diff --git a/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt index 47e67611..55acb067 100644 --- a/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt +++ b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt @@ -3,12 +3,9 @@ package io.github.taetae98coding.diary.domain.fetch.usecase import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.fetch.repository.FetchRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.first import org.koin.core.annotation.Factory -@OptIn(ExperimentalCoroutinesApi::class) @Factory public class FetchUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, @@ -16,16 +13,10 @@ public class FetchUseCase internal constructor( ) { public suspend operator fun invoke(): Result { return runCatching { - getAccountUseCase().mapLatest { it.getOrNull() } - .collectLatest { account -> - if (account is Account.Member) { - runCatching { fetch(account.uid) } - } - } + val account = getAccountUseCase().first().getOrThrow() + if (account is Account.Member) { + repository.fetchMemo(account.uid) + } } } - - private suspend fun fetch(uid: String) { - repository.fetchMemo(uid) - } } diff --git a/app/domain/memo/build.gradle.kts b/app/domain/memo/build.gradle.kts index 84e3b1c2..7385bf5b 100644 --- a/app/domain/memo/build.gradle.kts +++ b/app/domain/memo/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(project(":app:domain:account")) + implementation(project(":app:domain:backup")) } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt index 3a33d8bc..240a8b0b 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt @@ -6,10 +6,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.LocalDate public interface MemoRepository { - public suspend fun upsert(uid: String?, memo: Memo) - public suspend fun update(uid: String?, memoId: String, detail: MemoDetail) - public suspend fun updateFinish(uid: String?, memoId: String, isFinish: Boolean) - public suspend fun updateDelete(uid: String?, memoId: String, isDelete: Boolean) + public suspend fun upsert(memo: Memo) + public suspend fun update(memoId: String, detail: MemoDetail) + public suspend fun updateFinish(memoId: String, isFinish: Boolean) + public suspend fun updateDelete(memoId: String, isDelete: Boolean) public fun find(memoId: String): Flow public fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt index 7ae5cee3..8917b46b 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt @@ -1,13 +1,18 @@ package io.github.taetae98coding.diary.domain.memo.usecase import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.core.model.memo.Memo import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.datetime.Clock import org.koin.core.annotation.Factory @@ -17,6 +22,9 @@ public class AddMemoUseCase internal constructor( private val clock: Clock, private val getAccountUseCase: GetAccountUseCase, private val repository: MemoRepository, + private val coroutineScope: CoroutineScope, + private val backupRepository: BackupRepository, + private val backupUseCase: BackupUseCase, ) { public suspend operator fun invoke(detail: MemoDetail): Result { return runCatching { @@ -34,7 +42,14 @@ public class AddMemoUseCase internal constructor( updateAt = clock.now(), ) - repository.upsert(account.uid, memo) + repository.upsert(memo) + + if (account is Account.Member) { + coroutineScope.launch { + backupRepository.upsertMemoBackupQueue(account.uid, id) + backupUseCase() + } + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt index f223acc8..3b8cd293 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt @@ -1,21 +1,35 @@ package io.github.taetae98coding.diary.domain.memo.usecase +import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.koin.core.annotation.Factory @Factory public class DeleteMemoUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, private val repository: MemoRepository, + private val coroutineScope: CoroutineScope, + private val backupRepository: BackupRepository, + private val backupUseCase: BackupUseCase, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching val account = getAccountUseCase().first().getOrThrow() - repository.updateDelete(account.uid, memoId, true) + repository.updateDelete(memoId, true) + if (account is Account.Member) { + coroutineScope.launch { + backupRepository.upsertMemoBackupQueue(account.uid, memoId) + backupUseCase() + } + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt index f50e2376..dcd3d87b 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt @@ -1,21 +1,35 @@ package io.github.taetae98coding.diary.domain.memo.usecase +import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.koin.core.annotation.Factory @Factory public class FinishMemoUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, private val repository: MemoRepository, + private val coroutineScope: CoroutineScope, + private val backupRepository: BackupRepository, + private val backupUseCase: BackupUseCase, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching val account = getAccountUseCase().first().getOrThrow() - repository.updateFinish(account.uid, memoId, true) + repository.updateFinish(memoId, true) + if (account is Account.Member) { + coroutineScope.launch { + backupRepository.upsertMemoBackupQueue(account.uid, memoId) + backupUseCase() + } + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt index 66e7abdf..4bcdca0c 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt @@ -1,21 +1,36 @@ package io.github.taetae98coding.diary.domain.memo.usecase +import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.koin.core.annotation.Factory @Factory public class RestartMemoUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, private val repository: MemoRepository, + private val coroutineScope: CoroutineScope, + private val backupRepository: BackupRepository, + private val backupUseCase: BackupUseCase, ) { public suspend operator fun invoke(memoId: String?): Result { return runCatching { if (memoId.isNullOrBlank()) return@runCatching val account = getAccountUseCase().first().getOrThrow() - repository.updateFinish(account.uid, memoId, false) + + repository.updateFinish(memoId, false) + if (account is Account.Member) { + coroutineScope.launch { + backupRepository.upsertMemoBackupQueue(account.uid, memoId) + backupUseCase() + } + } } } } diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt index 81fa18bd..de589368 100644 --- a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt @@ -1,15 +1,23 @@ package io.github.taetae98coding.diary.domain.memo.usecase +import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.core.model.memo.MemoDetail import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.koin.core.annotation.Factory @Factory public class UpdateMemoUseCase internal constructor( private val getAccountUseCase: GetAccountUseCase, private val repository: MemoRepository, + private val coroutineScope: CoroutineScope, + private val backupRepository: BackupRepository, + private val backupUseCase: BackupUseCase, ) { public suspend operator fun invoke(memoId: String?, detail: MemoDetail): Result { return runCatching { @@ -19,8 +27,14 @@ public class UpdateMemoUseCase internal constructor( val account = getAccountUseCase().first().getOrThrow() val validDetail = detail.copy(title = detail.title.ifBlank { memo.detail.title }) - if (memo.detail != validDetail) { - repository.update(account.uid, memoId, validDetail) + if (memo.detail == validDetail) return@runCatching + + repository.update(memoId, validDetail) + if (account is Account.Member) { + coroutineScope.launch { + backupRepository.upsertMemoBackupQueue(account.uid, memoId) + backupUseCase() + } } } } diff --git a/app/feature/account/build.gradle.kts b/app/feature/account/build.gradle.kts index 57f5f7c9..debd7606 100644 --- a/app/feature/account/build.gradle.kts +++ b/app/feature/account/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(project(":app:domain:account")) + implementation(project(":app:domain:credential")) } } } diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt index 202de718..fd13bd4d 100644 --- a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.taetae98coding.diary.common.exception.NetworkException import io.github.taetae98coding.diary.common.exception.account.ExistEmailException -import io.github.taetae98coding.diary.domain.account.usecase.JoinUseCase -import io.github.taetae98coding.diary.domain.account.usecase.LoginUseCase +import io.github.taetae98coding.diary.domain.credential.usecase.JoinUseCase +import io.github.taetae98coding.diary.domain.credential.usecase.LoginUseCase import io.github.taetae98coding.diary.feature.account.join.state.JoinUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt index 6bbbc335..47062836 100644 --- a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.taetae98coding.diary.common.exception.NetworkException import io.github.taetae98coding.diary.common.exception.account.AccountNotFoundException -import io.github.taetae98coding.diary.domain.account.usecase.LoginUseCase +import io.github.taetae98coding.diary.domain.credential.usecase.LoginUseCase import io.github.taetae98coding.diary.feature.account.login.state.LoginUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/feature/more/build.gradle.kts b/app/feature/more/build.gradle.kts index 278a55e8..098c2778 100644 --- a/app/feature/more/build.gradle.kts +++ b/app/feature/more/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(project(":app:domain:account")) + implementation(project(":app:domain:credential")) } } } diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt index a77ccade..c6647e30 100644 --- a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase -import io.github.taetae98coding.diary.domain.account.usecase.LogoutUseCase +import io.github.taetae98coding.diary.domain.credential.usecase.LogoutUseCase import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/platform/android/build.gradle.kts b/app/platform/android/build.gradle.kts index 3f212970..b901dfac 100644 --- a/app/platform/android/build.gradle.kts +++ b/app/platform/android/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.android.firebase.perf) alias(libs.plugins.google.services) alias(libs.plugins.dependency.guard) + alias(libs.plugins.ksp) } android { @@ -92,6 +93,7 @@ dependencies { implementation(project(":app:core:holiday-preferences-datastore")) implementation(project(":app:core:holiday-database-room")) implementation(project(":app:core:holiday-service")) + implementation(project(":app:domain:fcm")) implementation(libs.android.material) implementation(libs.androidx.activity.compose) @@ -101,9 +103,14 @@ dependencies { implementation(libs.android.firebase.analytics) implementation(libs.android.firebase.crashlytics) implementation(libs.android.firebase.perf) + implementation(libs.android.firebase.messaging) implementation(platform(libs.koin.bom)) implementation(libs.koin.android) + implementation(platform(libs.koin.annotations.bom)) + implementation(libs.koin.annotations) + ksp(platform(libs.koin.annotations.bom)) + ksp(libs.koin.compiler) runtimeOnly(libs.ktor.client.okhttp) diff --git a/app/platform/android/src/main/AndroidManifest.xml b/app/platform/android/src/main/AndroidManifest.xml index b76fae4c..76ff8a94 100644 --- a/app/platform/android/src/main/AndroidManifest.xml +++ b/app/platform/android/src/main/AndroidManifest.xml @@ -3,13 +3,14 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + + + + - + + + + + + diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt index 06f2728f..6662c3bc 100644 --- a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt @@ -1,17 +1,26 @@ package io.github.taetae98coding.diary +import android.Manifest import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import io.github.taetae98coding.diary.app.App public class DiaryActivity : ComponentActivity() { + private val notificationPermissionLauncer = registerForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + callback = {} + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { App() } + + notificationPermissionLauncer.launch(Manifest.permission.POST_NOTIFICATIONS) } } diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt index 2d2af98d..9de0ba24 100644 --- a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt @@ -1,29 +1,5 @@ package io.github.taetae98coding.diary import android.app.Application -import androidx.lifecycle.LifecycleOwner -import io.github.taetae98coding.diary.app.manager.BackupManager -import io.github.taetae98coding.diary.app.manager.FetchManager -import org.koin.android.ext.android.get -public class DiaryApplication : Application() { - override fun onCreate() { - super.onCreate() - initBackupManager() - initFetchManager() - } - - private fun initBackupManager() { - val appLifecycleOwner = get() - val backupManager = get() - - backupManager.attach(appLifecycleOwner) - } - - private fun initFetchManager() { - val appLifecycleOwner = get() - val fetchManager = get() - - fetchManager.attach(appLifecycleOwner) - } -} +public class DiaryApplication : Application() diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/KoinAndroidModule.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/KoinAndroidModule.kt new file mode 100644 index 00000000..42b6268e --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/KoinAndroidModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +internal class KoinAndroidModule diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt new file mode 100644 index 00000000..cfead32a --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.initializer + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.startup.Initializer +import io.github.taetae98coding.diary.app.manager.BackupManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class BackupManagerInitializer : Initializer, KoinComponent { + private val manager by inject() + private val appLifecycleOwner by inject() + + override fun create(context: Context): BackupManager { + manager.attach(appLifecycleOwner) + + return manager + } + + override fun dependencies(): MutableList>> { + return mutableListOf(KoinInitializer::class.java) + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FCMManagerInitializer.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FCMManagerInitializer.kt new file mode 100644 index 00000000..aad4df4a --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FCMManagerInitializer.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.initializer + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.startup.Initializer +import io.github.taetae98coding.diary.app.manager.FCMManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class FCMManagerInitializer : Initializer, KoinComponent { + private val manager by inject() + private val appLifecycleOwner by inject() + + override fun create(context: Context): FCMManager { + manager.attach(appLifecycleOwner) + + return manager + } + + override fun dependencies(): MutableList>> { + return mutableListOf(KoinInitializer::class.java) + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt new file mode 100644 index 00000000..b85bffb1 --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.initializer + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.startup.Initializer +import io.github.taetae98coding.diary.app.manager.FetchManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public class FetchManagerInitializer : Initializer, KoinComponent { + private val manager by inject() + private val appLifecycleOwner by inject() + + override fun create(context: Context): FetchManager { + manager.attach(appLifecycleOwner) + + return manager + } + + override fun dependencies(): MutableList>> { + return mutableListOf(KoinInitializer::class.java) + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt index 749e953e..6e6f32cc 100644 --- a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt @@ -3,6 +3,7 @@ package io.github.taetae98coding.diary.initializer import android.content.Context import androidx.startup.Initializer import io.github.taetae98coding.diary.BuildConfig +import io.github.taetae98coding.diary.KoinAndroidModule import io.github.taetae98coding.diary.app.AppModule import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule import io.github.taetae98coding.diary.core.diary.database.room.DiaryRoomDatabaseModule @@ -24,6 +25,7 @@ public class KoinInitializer : Initializer { androidContext(context) modules( + KoinAndroidModule().module, AppModule().module, diaryServiceModule(), AccountDataStorePreferencesModule().module, diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/notification/DefaultNotificationManager.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/notification/DefaultNotificationManager.kt new file mode 100644 index 00000000..040cc419 --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/notification/DefaultNotificationManager.kt @@ -0,0 +1,41 @@ +package io.github.taetae98coding.diary.notification + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import io.github.taetae98coding.diary.R +import kotlin.math.abs +import org.koin.core.annotation.Factory + +@Factory +internal class DefaultNotificationManager( + private val context: Context, +) { + fun notify(title: String, description: String?) { + if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return + + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_android_black_24dp) + .setContentTitle(title) + .setContentText(description) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + + val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) + .setName(context.getString(R.string.default_channel_name)) + .setDescription(context.getString(R.string.default_channel_description)) + .build() + + val manager = NotificationManagerCompat.from(context) + + manager.createNotificationChannel(channel) + manager.notify(abs(System.currentTimeMillis().toInt()), notification) + } + + companion object { + const val CHANNEL_ID = "Diary" + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/service/DiaryFirebaseMessagingService.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/service/DiaryFirebaseMessagingService.kt new file mode 100644 index 00000000..55db41b7 --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/service/DiaryFirebaseMessagingService.kt @@ -0,0 +1,45 @@ +package io.github.taetae98coding.diary.service + +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import io.github.taetae98coding.diary.domain.fcm.usecase.UpdateFCMTokenUseCase +import io.github.taetae98coding.diary.notification.DefaultNotificationManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject + +public class DiaryFirebaseMessagingService : FirebaseMessagingService() { + private val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + + private val defaultNotificationManager by inject() + private val updateFCMTokenUseCase by inject() + + override fun onNewToken(token: String) { + super.onNewToken(token) + serviceScope.launch { updateFCMTokenUseCase() } + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + when (message.data[TYPE]) { + else -> { + defaultNotificationManager.notify( + title = message.notification?.title.orEmpty(), + description = message.notification?.body, + ) + } + } + } + + override fun onDestroy() { + super.onDestroy() + serviceScope.cancel() + } + + public companion object { + private const val TYPE = "type" + } +} diff --git a/app/platform/android/src/main/res/drawable/ic_android_black_24dp.xml b/app/platform/android/src/main/res/drawable/ic_android_black_24dp.xml new file mode 100644 index 00000000..171b3901 --- /dev/null +++ b/app/platform/android/src/main/res/drawable/ic_android_black_24dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/platform/android/src/main/res/values-ko/strings.xml b/app/platform/android/src/main/res/values-ko/strings.xml new file mode 100644 index 00000000..f04fc6fe --- /dev/null +++ b/app/platform/android/src/main/res/values-ko/strings.xml @@ -0,0 +1,5 @@ + + + 다이어리 + 다이어리 알림. + \ No newline at end of file diff --git a/app/platform/android/src/main/res/values/strings.xml b/app/platform/android/src/main/res/values/strings.xml new file mode 100644 index 00000000..d8941a94 --- /dev/null +++ b/app/platform/android/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Diary + Diary notification. + \ No newline at end of file diff --git a/app/platform/common/build.gradle.kts b/app/platform/common/build.gradle.kts index a1ea8f56..209b0741 100644 --- a/app/platform/common/build.gradle.kts +++ b/app/platform/common/build.gradle.kts @@ -8,15 +8,19 @@ kotlin { dependencies { implementation(project(":app:data:memo")) implementation(project(":app:data:account")) + implementation(project(":app:data:credential")) implementation(project(":app:data:holiday")) implementation(project(":app:data:backup")) implementation(project(":app:data:fetch")) + implementation(project(":app:data:fcm")) implementation(project(":app:domain:memo")) implementation(project(":app:domain:account")) + implementation(project(":app:domain:credential")) implementation(project(":app:domain:holiday")) implementation(project(":app:domain:backup")) implementation(project(":app:domain:fetch")) + implementation(project(":app:domain:fcm")) implementation(project(":app:core:coroutines")) implementation(project(":app:core:diary-service")) @@ -28,6 +32,7 @@ kotlin { implementation(project(":app:feature:account")) implementation(project(":library:datetime")) + implementation(project(":library:firebase-messaging")) implementation(compose.material3AdaptiveNavigationSuite) implementation(libs.compose.material3.adaptive) diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt index a7e70294..bb6b99bd 100644 --- a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt @@ -5,11 +5,15 @@ import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule import io.github.taetae98coding.diary.data.account.AccountDataModule import io.github.taetae98coding.diary.data.backup.BackupDataModule +import io.github.taetae98coding.diary.data.credential.CredentialDataModule +import io.github.taetae98coding.diary.data.fcm.FCMDataModule import io.github.taetae98coding.diary.data.fetch.FetchDataModule import io.github.taetae98coding.diary.data.holiday.HolidayDataModule import io.github.taetae98coding.diary.data.memo.MemoDataModule import io.github.taetae98coding.diary.domain.account.AccountDomainModule import io.github.taetae98coding.diary.domain.backup.BackupDomainModule +import io.github.taetae98coding.diary.domain.credential.CredentialDomainModule +import io.github.taetae98coding.diary.domain.fcm.FCMDomainModule import io.github.taetae98coding.diary.domain.fetch.FetchDomainModule import io.github.taetae98coding.diary.domain.holiday.HolidayDomainModule import io.github.taetae98coding.diary.domain.memo.MemoDomainModule @@ -17,6 +21,9 @@ import io.github.taetae98coding.diary.feature.account.AccountFeatureModule import io.github.taetae98coding.diary.feature.calendar.CalendarFeatureModule import io.github.taetae98coding.diary.feature.memo.MemoFeatureModule import io.github.taetae98coding.diary.feature.more.MoreFeatureModule +import io.github.taetae98coding.diary.library.firebase.KFirebase +import io.github.taetae98coding.diary.library.firebase.messaging.KFirebaseMessaging +import io.github.taetae98coding.diary.library.firebase.messaging.messaging import kotlinx.datetime.Clock import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module @@ -32,11 +39,15 @@ import org.koin.core.annotation.Singleton HolidayDataModule::class, BackupDataModule::class, FetchDataModule::class, + FCMDataModule::class, + CredentialDataModule::class, MemoDomainModule::class, AccountDomainModule::class, HolidayDomainModule::class, BackupDomainModule::class, FetchDomainModule::class, + FCMDomainModule::class, + CredentialDomainModule::class, MemoFeatureModule::class, CalendarFeatureModule::class, MoreFeatureModule::class, @@ -49,4 +60,9 @@ public class AppModule { internal fun providesClock(): Clock { return Clock.System } + + @Singleton + internal fun providesFirebaseMessaging(): KFirebaseMessaging { + return KFirebase.messaging + } } diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FCMManager.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FCMManager.kt new file mode 100644 index 00000000..34095ce0 --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FCMManager.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.app.manager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import io.github.taetae98coding.diary.domain.fcm.usecase.UpdateFCMTokenUseCase +import kotlinx.coroutines.launch +import org.koin.core.annotation.Singleton + +@Singleton +public class FCMManager internal constructor( + private val updateFCMTokenUseCase: UpdateFCMTokenUseCase, +) { + public fun attach(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + updateFCMTokenUseCase() + } + } + } +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/InitFirebaseMessagingManaver.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/InitFirebaseMessagingManaver.kt new file mode 100644 index 00000000..07e533b6 --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/InitFirebaseMessagingManaver.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import io.github.taetae98coding.diary.app.manager.FCMManager +import kotlinx.coroutines.launch +import org.koin.core.KoinApplication + +internal fun initFirebaseMessagingManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val fcmManager = koinApplication.koin.get() + + appLifecycleOwner.lifecycleScope.launch { + fcmManager.attach(appLifecycleOwner) + } +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt index 4211f419..29ebdbc5 100644 --- a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt @@ -6,4 +6,5 @@ public fun init() { initBackupManager(koinApplication) initFetchManager(koinApplication) + initFirebaseMessagingManager(koinApplication) } diff --git a/build.gradle.kts b/build.gradle.kts index e408b5c7..a0a315f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.kotlin.android).apply(false) alias(libs.plugins.kotlin.jvm).apply(false) + alias(libs.plugins.kotlin.cocoapods).apply(false) alias(libs.plugins.kotlin.serialization).apply(false) alias(libs.plugins.ksp).apply(false) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/DeleteFCMRequest.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/DeleteFCMRequest.kt new file mode 100644 index 00000000..750d8f6b --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/DeleteFCMRequest.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.common.model.request.fcm + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class DeleteFCMRequest( + @SerialName("token") + val token: String, +) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/UpsertFCMRequest.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/UpsertFCMRequest.kt new file mode 100644 index 00000000..de8f6260 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/fcm/UpsertFCMRequest.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.common.model.request.fcm + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class UpsertFCMRequest( + @SerialName("token") + val token: String, +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3498f555..7ed1dead 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,10 +10,10 @@ kotlinx-coroutines = "1.9.0" # https://github.com/Kotlin/kotlinx. kotlinx-datetime = "0.6.1" # https://github.com/Kotlin/kotlinx-datetime/releases ### multiplatform -compose = "1.7.0" # https://github.com/JetBrains/compose-multiplatform/releases -compose-material3-adaptive = "1.0.0" +compose = "1.7.1" # https://github.com/JetBrains/compose-multiplatform/releases +compose-material3-adaptive = "1.0.1" navigation = "2.8.0-alpha10" -lifecycle = "2.8.3" +lifecycle = "2.8.4" androidx-lifecycle = "2.8.5" compose-markdown = "0.27.0" # https://github.com/mikepenz/multiplatform-markdown-renderer/releases @@ -103,6 +103,7 @@ android-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", v android-firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } android-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } android-firebase-perf = { group = "com.google.firebase", name = "firebase-perf" } +android-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } @@ -136,6 +137,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/library/firebase-common/build.gradle.kts b/library/firebase-common/build.gradle.kts new file mode 100644 index 00000000..9992a217 --- /dev/null +++ b/library/firebase-common/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") +} + +android { + namespace = "${Build.NAMESPACE}.library.firebase.common" +} diff --git a/library/firebase-common/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/KFirebase.kt b/library/firebase-common/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/KFirebase.kt new file mode 100644 index 00000000..c8831124 --- /dev/null +++ b/library/firebase-common/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/KFirebase.kt @@ -0,0 +1,3 @@ +package io.github.taetae98coding.diary.library.firebase + +public data object KFirebase diff --git a/library/firebase-messaging/build.gradle.kts b/library/firebase-messaging/build.gradle.kts new file mode 100644 index 00000000..4ba6de4c --- /dev/null +++ b/library/firebase-messaging/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + alias(libs.plugins.kotlin.cocoapods) +} + +kotlin { + cocoapods { + noPodspec() + + ios.deploymentTarget = "18.0" + + pod("FirebaseMessaging") { + version = "11.4.0" + } + } + + sourceSets { + commonMain { + dependencies { + api(project(":library:firebase-common")) + } + } + + androidMain { + dependencies { + implementation(project.dependencies.platform(libs.android.firebase.bom)) + implementation(libs.android.firebase.messaging) + } + } + + iosMain { + dependencies { + implementation(libs.kotlinx.coroutines.core) + } + } + + val nonSupportMain = create("nonSupportMain") + + nonSupportMain.dependsOn(commonMain.get()) + jvmMain.get().dependsOn(nonSupportMain) + wasmJsMain.get().dependsOn(nonSupportMain) + } +} + +android { + namespace = "${Build.NAMESPACE}.library.firebase.messaging" +} diff --git a/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.android.kt b/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.android.kt new file mode 100644 index 00000000..27942c24 --- /dev/null +++ b/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.android.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import io.github.taetae98coding.diary.library.firebase.KFirebase + +public actual val KFirebase.messaging: KFirebaseMessaging + get() = KFirebaseMessagingImpl() diff --git a/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt b/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt new file mode 100644 index 00000000..b1ea0049 --- /dev/null +++ b/library/firebase-messaging/src/androidMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import com.google.firebase.Firebase +import com.google.firebase.messaging.messaging +import kotlinx.coroutines.tasks.await + +internal class KFirebaseMessagingImpl : KFirebaseMessaging { + override suspend fun getToken(): String { + return Firebase.messaging.token.await() + } +} diff --git a/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessaging.kt b/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessaging.kt new file mode 100644 index 00000000..e879128d --- /dev/null +++ b/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessaging.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +public interface KFirebaseMessaging { + public suspend fun getToken(): String +} diff --git a/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.kt b/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.kt new file mode 100644 index 00000000..35ed1fe6 --- /dev/null +++ b/library/firebase-messaging/src/commonMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import io.github.taetae98coding.diary.library.firebase.KFirebase + +public expect val KFirebase.messaging: KFirebaseMessaging diff --git a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt b/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt new file mode 100644 index 00000000..27942c24 --- /dev/null +++ b/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.ios.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import io.github.taetae98coding.diary.library.firebase.KFirebase + +public actual val KFirebase.messaging: KFirebaseMessaging + get() = KFirebaseMessagingImpl() diff --git a/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt b/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt new file mode 100644 index 00000000..d9c365fa --- /dev/null +++ b/library/firebase-messaging/src/iosMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import cocoapods.FirebaseMessaging.FIRMessaging +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalForeignApi::class) +internal class KFirebaseMessagingImpl : KFirebaseMessaging { + override suspend fun getToken(): String { + return suspendCancellableCoroutine { continuation -> + FIRMessaging.messaging().tokenWithCompletion { token, error -> + if (error != null) { + continuation.resumeWithException(Exception(error.toString())) + } else if (token.isNullOrBlank()) { + continuation.resumeWithException(Exception("token is null or blank")) + } else { + continuation.resume(token) + } + } + } + } +} diff --git a/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.nonSupport.kt b/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.nonSupport.kt new file mode 100644 index 00000000..27942c24 --- /dev/null +++ b/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingExt.nonSupport.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +import io.github.taetae98coding.diary.library.firebase.KFirebase + +public actual val KFirebase.messaging: KFirebaseMessaging + get() = KFirebaseMessagingImpl() diff --git a/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt b/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt new file mode 100644 index 00000000..6f25db4f --- /dev/null +++ b/library/firebase-messaging/src/nonSupportMain/kotlin/io/github/taetae98coding/diary/library/firebase/messaging/KFirebaseMessagingImpl.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.library.firebase.messaging + +internal class KFirebaseMessagingImpl : KFirebaseMessaging { + override suspend fun getToken(): String { + error("Not Support") + } +} diff --git a/server/app/build.gradle.kts b/server/app/build.gradle.kts index 9f2cc389..72f55ec2 100644 --- a/server/app/build.gradle.kts +++ b/server/app/build.gradle.kts @@ -13,13 +13,16 @@ dependencies { implementation(project(":server:data:account")) implementation(project(":server:data:memo")) + implementation(project(":server:data:fcm")) implementation(project(":server:domain:account")) implementation(project(":server:domain:memo")) + implementation(project(":server:domain:fcm")) implementation(project(":server:feature:home")) implementation(project(":server:feature:account")) implementation(project(":server:feature:memo")) + implementation(project(":server:feature:fcm")) implementation(project(":common:model")) diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt index 56fa0def..f0404ae7 100644 --- a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.plugin import io.github.taetae98coding.diary.core.database.AccountTable +import io.github.taetae98coding.diary.core.database.FCMTokenTable import io.github.taetae98coding.diary.core.database.MemoTable import io.ktor.server.application.Application import org.jetbrains.exposed.sql.Database @@ -8,15 +9,15 @@ import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction internal fun Application.installDatabase() { - val database = - Database.connect( - url = environment.config.property("database.url").getString(), - driver = "com.mysql.cj.jdbc.Driver", - user = environment.config.property("database.user").getString(), - password = environment.config.property("database.password").getString(), - ) + val database = + Database.connect( + url = environment.config.property("database.url").getString(), + driver = "com.mysql.cj.jdbc.Driver", + user = environment.config.property("database.user").getString(), + password = environment.config.property("database.password").getString(), + ) - transaction(database) { - SchemaUtils.createMissingTablesAndColumns(AccountTable, MemoTable) - } + transaction(database) { + SchemaUtils.createMissingTablesAndColumns(AccountTable, MemoTable, FCMTokenTable) + } } diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt index 24f8a0d5..5f17d638 100644 --- a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt @@ -1,8 +1,10 @@ package io.github.taetae98coding.diary.plugin import io.github.taetae98coding.diary.data.account.AccountDataModule +import io.github.taetae98coding.diary.data.fcm.FCMDataModule import io.github.taetae98coding.diary.data.memo.MemoDataModule import io.github.taetae98coding.diary.domain.account.AccountDomainModule +import io.github.taetae98coding.diary.domain.fcm.FCMDomainModule import io.github.taetae98coding.diary.domain.memo.MemoDomainModule import io.ktor.server.application.Application import io.ktor.server.application.install @@ -10,12 +12,14 @@ import org.koin.ksp.generated.module import org.koin.ktor.plugin.Koin internal fun Application.installKoin() { - install(Koin) { - modules( - AccountDataModule().module, - MemoDataModule().module, - AccountDomainModule().module, - MemoDomainModule().module, - ) - } + install(Koin) { + modules( + AccountDataModule().module, + MemoDataModule().module, + FCMDataModule().module, + AccountDomainModule().module, + MemoDomainModule().module, + FCMDomainModule().module, + ) + } } diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt index ad0d347a..d610d795 100644 --- a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt @@ -1,6 +1,7 @@ package io.github.taetae98coding.diary.plugin import io.github.taetae98coding.diary.feature.account.accountRouting +import io.github.taetae98coding.diary.feature.fcm.fcmRouting import io.github.taetae98coding.diary.feature.home.homeRouting import io.github.taetae98coding.diary.feature.memo.memoRouting import io.ktor.server.application.Application @@ -11,5 +12,6 @@ internal fun Application.installRouting() { homeRouting() accountRouting() memoRouting() + fcmRouting() } } diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt new file mode 100644 index 00000000..edc5f6e3 --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/FCMTokenTable.kt @@ -0,0 +1,40 @@ +package io.github.taetae98coding.diary.core.database + +import kotlinx.datetime.Clock +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.upsert + +public data object FCMTokenTable : Table(name = "FCMToken") { + private val TOKEN = varchar("token", 255).default("") + private val OWNER = reference( + name = "owner", + refColumn = AccountTable.UID, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ).nullable() + + private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) + + override val primaryKey: PrimaryKey = PrimaryKey(TOKEN) + + public suspend fun upsert(token: String, owner: String) { + newSuspendedTransaction { + upsert { + it[TOKEN] = token + it[OWNER] = owner + it[UPDATE_AT] = Clock.System.now() + } + } + } + + public suspend fun delete(token: String) { + newSuspendedTransaction { + deleteWhere { TOKEN eq token } + } + } +} diff --git a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt index b3a4b2e1..350dbc3d 100644 --- a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt +++ b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt @@ -13,5 +13,5 @@ internal class AccountRepositoryImpl : AccountRepository { AccountTable.insert(account) } - override suspend fun findByEmail(email: String, password: String): Account? = AccountTable.findByEmail(email, password) + override suspend fun findByEmail(email: String, password: String): Account? = AccountTable.findByEmail(email, password) } diff --git a/server/data/fcm/build.gradle.kts b/server/data/fcm/build.gradle.kts new file mode 100644 index 00000000..d4b84fbe --- /dev/null +++ b/server/data/fcm/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("diary.server.data") +} + +dependencies { + implementation(project(":server:core:database")) + implementation(project(":server:domain:fcm")) + + implementation(platform(libs.exposed.bom)) + implementation(libs.exposed.core) +} diff --git a/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt new file mode 100644 index 00000000..e9adca4d --- /dev/null +++ b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/FCMDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.fcm + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FCMDataModule diff --git a/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt new file mode 100644 index 00000000..049e59ac --- /dev/null +++ b/server/data/fcm/src/main/kotlin/io/github/taetae98coding/diary/data/fcm/repository/FCMRepositoryImpl.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.data.fcm.repository + +import io.github.taetae98coding.diary.core.database.FCMTokenTable +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import org.koin.core.annotation.Factory + +@Factory +internal class FCMRepositoryImpl : FCMRepository { + override suspend fun upsert(token: String, owner: String) { + FCMTokenTable.upsert(token, owner) + } + + override suspend fun delete(token: String) { + FCMTokenTable.delete(token) + } +} diff --git a/server/domain/fcm/build.gradle.kts b/server/domain/fcm/build.gradle.kts new file mode 100644 index 00000000..e10cade2 --- /dev/null +++ b/server/domain/fcm/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.server.domain") +} diff --git a/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt new file mode 100644 index 00000000..a2d83583 --- /dev/null +++ b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/FCMDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.fcm + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FCMDomainModule diff --git a/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt new file mode 100644 index 00000000..54615a0b --- /dev/null +++ b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/repository/FCMRepository.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.domain.fcm.repository + +public interface FCMRepository { + public suspend fun upsert(token: String, owner: String) + public suspend fun delete(token: String) +} diff --git a/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/DeleteFCMTokenUseCase.kt b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/DeleteFCMTokenUseCase.kt new file mode 100644 index 00000000..d8394b74 --- /dev/null +++ b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/DeleteFCMTokenUseCase.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.domain.fcm.usecase + +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import org.koin.core.annotation.Factory + +@Factory +public class DeleteFCMTokenUseCase internal constructor( + private val repository: FCMRepository +) { + public suspend operator fun invoke(token: String): Result { + return runCatching { repository.delete(token) } + } +} diff --git a/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpsertFCMTokenUseCase.kt b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpsertFCMTokenUseCase.kt new file mode 100644 index 00000000..377e0878 --- /dev/null +++ b/server/domain/fcm/src/main/kotlin/io/github/taetae98coding/diary/domain/fcm/usecase/UpsertFCMTokenUseCase.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.domain.fcm.usecase + +import io.github.taetae98coding.diary.domain.fcm.repository.FCMRepository +import org.koin.core.annotation.Factory + +@Factory +public class UpsertFCMTokenUseCase internal constructor( + private val repository: FCMRepository, +) { + public suspend operator fun invoke(token: String, owner: String): Result { + return runCatching { repository.upsert(token, owner) } + } +} diff --git a/server/feature/fcm/build.gradle.kts b/server/feature/fcm/build.gradle.kts new file mode 100644 index 00000000..9dacd93b --- /dev/null +++ b/server/feature/fcm/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("diary.server.feature") +} + +dependencies { + implementation(project(":server:domain:fcm")) +} diff --git a/server/feature/fcm/src/main/kotlin/io/github/taetae98coding/diary/feature/fcm/FCMRouting.kt b/server/feature/fcm/src/main/kotlin/io/github/taetae98coding/diary/feature/fcm/FCMRouting.kt new file mode 100644 index 00000000..36b1a1e1 --- /dev/null +++ b/server/feature/fcm/src/main/kotlin/io/github/taetae98coding/diary/feature/fcm/FCMRouting.kt @@ -0,0 +1,46 @@ +package io.github.taetae98coding.diary.feature.fcm + +import io.github.taetae98coding.diary.common.model.request.fcm.DeleteFCMRequest +import io.github.taetae98coding.diary.common.model.request.fcm.UpsertFCMRequest +import io.github.taetae98coding.diary.common.model.response.DiaryResponse +import io.github.taetae98coding.diary.domain.fcm.usecase.DeleteFCMTokenUseCase +import io.github.taetae98coding.diary.domain.fcm.usecase.UpsertFCMTokenUseCase +import io.ktor.http.HttpStatusCode +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.jwt.JWTPrincipal +import io.ktor.server.auth.principal +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import org.koin.ktor.plugin.scope + +public fun Route.fcmRouting() { + route("/fcm") { + post("/delete") { request -> + val useCase = call.scope.get() + + useCase(token = request.token) + .onSuccess { call.respond(DiaryResponse.Success) } + } + + authenticate("account") { + post("/upsert") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@post + } + + val useCase = call.scope.get() + + useCase( + token = request.token, + owner = principal.payload.getClaim("uid").asString(), + ).onSuccess { + call.respond(DiaryResponse.Success) + } + } + } + } +} diff --git a/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt index 3e5bd5b5..3ed30933 100644 --- a/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt +++ b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt @@ -50,15 +50,6 @@ public fun Route.memoRouting() { .onSuccess { call.respond(DiaryResponse.success(it.map(Memo::toEntity))) } } } - - post>("/migrate") { request -> - val useCase = call.scope.get() - val memoList = request.map(MemoEntity::toMemo) - - useCase(memoList) - .onSuccess { call.respond(DiaryResponse.Success) } - .onFailure { call.respond(HttpStatusCode.InternalServerError, it.toString()) } - } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 146c4db6..0031627f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,12 +65,16 @@ include(":app:data:account") include(":app:data:holiday") include(":app:data:backup") include(":app:data:fetch") +include(":app:data:fcm") +include(":app:data:credential") include(":app:domain:memo") include(":app:domain:account") include(":app:domain:holiday") include(":app:domain:backup") include(":app:domain:fetch") +include(":app:domain:fcm") +include(":app:domain:credential") include(":app:feature:memo") include(":app:feature:calendar") @@ -82,14 +86,17 @@ include(":server:core:model") include(":server:data:account") include(":server:data:memo") +include(":server:data:fcm") include(":server:domain:account") include(":server:domain:memo") +include(":server:domain:fcm") include(":server:app") include(":server:feature:home") include(":server:feature:account") include(":server:feature:memo") +include(":server:feature:fcm") include(":common:exception") include(":common:model") @@ -103,3 +110,6 @@ include(":library:kotlin") include(":library:navigation") include(":library:room") include(":library:shimmer-m3") + +include(":library:firebase-common") +include(":library:firebase-messaging")