Repository: prof18/MoneyFlow Branch: main Commit: fd967b051efc Files: 490 Total size: 743.0 KB Directory structure: gitextract_z7ii1zwp/ ├── .claude/ │ └── settings.local.json ├── .github/ │ ├── FUNDING.yml │ ├── actions/ │ │ └── setup-gradle/ │ │ └── action.yml │ └── workflows/ │ ├── android-release.yml │ ├── checks.yml │ ├── ios-release.yml │ ├── release.yml │ └── roborazzi.yml ├── .gitignore ├── AGENTS.md ├── README.md ├── androidApp/ │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ ├── debug/ │ │ └── res/ │ │ └── mipmap-anydpi-v26/ │ │ └── ic_launcher.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── prof18/ │ │ └── moneyflow/ │ │ ├── MainActivity.kt │ │ └── MoneyFlowApp.kt │ └── release/ │ └── res/ │ └── mipmap-anydpi-v26/ │ └── ic_launcher.xml ├── build-logic/ │ ├── convention/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── DetektConventionPlugin.kt │ └── settings.gradle.kts ├── build.gradle.kts ├── config/ │ └── detekt/ │ └── detekt.yml ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── iosApp/ │ ├── .scripts/ │ │ └── version.sh │ ├── Assets/ │ │ ├── DebugIcon.icon/ │ │ │ └── icon.json │ │ ├── Icon.icon/ │ │ │ └── icon.json │ │ └── Info.plist │ ├── Configuration/ │ │ └── Config.xcconfig │ ├── MoneyFlow.entitlements │ ├── MoneyFlow.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── MoneyFlow.xcscheme │ └── Source/ │ ├── ContentView.swift │ ├── DI/ │ │ └── Koin.swift │ └── MoneyFlowApp.swift ├── renovate.json ├── settings.gradle.kts ├── setup.sh ├── shared/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── prof18/ │ │ │ └── moneyflow/ │ │ │ ├── AndroidBiometricAuthenticator.kt │ │ │ ├── AndroidBiometricAvailabilityChecker.kt │ │ │ ├── database/ │ │ │ │ └── DatabaseDriverFactory.kt │ │ │ ├── di/ │ │ │ │ └── KoinAndroid.kt │ │ │ └── utils/ │ │ │ ├── LocalAppLocale.android.kt │ │ │ └── LocalAppTheme.android.kt │ │ └── res/ │ │ ├── values/ │ │ │ └── themes.xml │ │ └── values-night/ │ │ └── themes.xml │ ├── androidUnitTest/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── prof18/ │ │ └── moneyflow/ │ │ ├── AddTransactionRoborazziTest.kt │ │ ├── AllTransactionsRoborazziTest.kt │ │ ├── AuthRoborazziTest.kt │ │ ├── BudgetAndRecapRoborazziTest.kt │ │ ├── CategoriesRoborazziTest.kt │ │ ├── ComponentsRoborazziTest.kt │ │ ├── HomeRoborazziTest.kt │ │ ├── MoneyFlowLockedRoborazziTest.kt │ │ ├── MoneyFlowNavHostRoborazziTest.kt │ │ ├── RoborazziRule.kt │ │ ├── RoborazziTestBase.kt │ │ ├── SettingsRoborazziTest.kt │ │ └── utilities/ │ │ └── TestUtilsAndroid.kt │ ├── commonMain/ │ │ ├── composeResources/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_address_book.xml │ │ │ │ ├── ic_address_card.xml │ │ │ │ ├── ic_adjust_solid.xml │ │ │ │ ├── ic_air_freshener_solid.xml │ │ │ │ ├── ic_algolia.xml │ │ │ │ ├── ic_allergies_solid.xml │ │ │ │ ├── ic_ambulance_solid.xml │ │ │ │ ├── ic_anchor_solid.xml │ │ │ │ ├── ic_android.xml │ │ │ │ ├── ic_angle_down_solid.xml │ │ │ │ ├── ic_angle_left_solid.xml │ │ │ │ ├── ic_angle_right_solid.xml │ │ │ │ ├── ic_angle_up_solid.xml │ │ │ │ ├── ic_apple.xml │ │ │ │ ├── ic_apple_alt_solid.xml │ │ │ │ ├── ic_archive_solid.xml │ │ │ │ ├── ic_archway_solid.xml │ │ │ │ ├── ic_arrow_down_rotate.xml │ │ │ │ ├── ic_arrow_down_solid.xml │ │ │ │ ├── ic_arrow_left_solid.xml │ │ │ │ ├── ic_arrow_right_solid.xml │ │ │ │ ├── ic_arrow_up_rotate.xml │ │ │ │ ├── ic_arrow_up_solid.xml │ │ │ │ ├── ic_asterisk_solid.xml │ │ │ │ ├── ic_at_solid.xml │ │ │ │ ├── ic_atlas_solid.xml │ │ │ │ ├── ic_atom_solid.xml │ │ │ │ ├── ic_award_solid.xml │ │ │ │ ├── ic_baby_carriage_solid.xml │ │ │ │ ├── ic_bacon_solid.xml │ │ │ │ ├── ic_balance_scale_left_solid.xml │ │ │ │ ├── ic_band_aid_solid.xml │ │ │ │ ├── ic_baseball_ball_solid.xml │ │ │ │ ├── ic_basketball_ball_solid.xml │ │ │ │ ├── ic_bath_solid.xml │ │ │ │ ├── ic_battery_three_quarters_solid.xml │ │ │ │ ├── ic_bed_solid.xml │ │ │ │ ├── ic_beer_solid.xml │ │ │ │ ├── ic_bell.xml │ │ │ │ ├── ic_bell_slash.xml │ │ │ │ ├── ic_bicycle_solid.xml │ │ │ │ ├── ic_biking_solid.xml │ │ │ │ ├── ic_binoculars_solid.xml │ │ │ │ ├── ic_birthday_cake_solid.xml │ │ │ │ ├── ic_bitcoin.xml │ │ │ │ ├── ic_black_tie.xml │ │ │ │ ├── ic_blender_solid.xml │ │ │ │ ├── ic_blind_solid.xml │ │ │ │ ├── ic_bolt_solid.xml │ │ │ │ ├── ic_bomb_solid.xml │ │ │ │ ├── ic_bone_solid.xml │ │ │ │ ├── ic_bong_solid.xml │ │ │ │ ├── ic_book_open_solid.xml │ │ │ │ ├── ic_book_solid.xml │ │ │ │ ├── ic_bookmark.xml │ │ │ │ ├── ic_bowling_ball_solid.xml │ │ │ │ ├── ic_box_solid.xml │ │ │ │ ├── ic_brain_solid.xml │ │ │ │ ├── ic_bread_slice_solid.xml │ │ │ │ ├── ic_briefcase_medical_solid.xml │ │ │ │ ├── ic_briefcase_solid.xml │ │ │ │ ├── ic_broadcast_tower_solid.xml │ │ │ │ ├── ic_broom_solid.xml │ │ │ │ ├── ic_brush_solid.xml │ │ │ │ ├── ic_bug_solid.xml │ │ │ │ ├── ic_building.xml │ │ │ │ ├── ic_bullhorn_solid.xml │ │ │ │ ├── ic_bullseye_solid.xml │ │ │ │ ├── ic_burn_solid.xml │ │ │ │ ├── ic_bus_solid.xml │ │ │ │ ├── ic_calculator_solid.xml │ │ │ │ ├── ic_calendar.xml │ │ │ │ ├── ic_camera_solid.xml │ │ │ │ ├── ic_campground_solid.xml │ │ │ │ ├── ic_candy_cane_solid.xml │ │ │ │ ├── ic_capsules_solid.xml │ │ │ │ ├── ic_car_alt_solid.xml │ │ │ │ ├── ic_car_side_solid.xml │ │ │ │ ├── ic_caret_down_solid.xml │ │ │ │ ├── ic_caret_left_solid.xml │ │ │ │ ├── ic_caret_right_solid.xml │ │ │ │ ├── ic_caret_up_solid.xml │ │ │ │ ├── ic_carrot_solid.xml │ │ │ │ ├── ic_cart_arrow_down_solid.xml │ │ │ │ ├── ic_cash_register_solid.xml │ │ │ │ ├── ic_cat_solid.xml │ │ │ │ ├── ic_certificate_solid.xml │ │ │ │ ├── ic_chair_solid.xml │ │ │ │ ├── ic_chalkboard_solid.xml │ │ │ │ ├── ic_chalkboard_teacher_solid.xml │ │ │ │ ├── ic_charging_station_solid.xml │ │ │ │ ├── ic_chart_area_solid.xml │ │ │ │ ├── ic_chart_bar.xml │ │ │ │ ├── ic_chart_line_solid.xml │ │ │ │ ├── ic_chart_pie_solid.xml │ │ │ │ ├── ic_check_circle.xml │ │ │ │ ├── ic_cheese_solid.xml │ │ │ │ ├── ic_church_solid.xml │ │ │ │ ├── ic_city_solid.xml │ │ │ │ ├── ic_clinic_medical_solid.xml │ │ │ │ ├── ic_clipboard.xml │ │ │ │ ├── ic_clock.xml │ │ │ │ ├── ic_cloud_download_alt_solid.xml │ │ │ │ ├── ic_cloud_solid.xml │ │ │ │ ├── ic_cloud_upload_alt_solid.xml │ │ │ │ ├── ic_cocktail_solid.xml │ │ │ │ ├── ic_code_branch_solid.xml │ │ │ │ ├── ic_code_solid.xml │ │ │ │ ├── ic_coffee_solid.xml │ │ │ │ ├── ic_cog_solid.xml │ │ │ │ ├── ic_coins_solid.xml │ │ │ │ ├── ic_comment_alt.xml │ │ │ │ ├── ic_compact_disc_solid.xml │ │ │ │ ├── ic_compass.xml │ │ │ │ ├── ic_concierge_bell_solid.xml │ │ │ │ ├── ic_cookie_bite_solid.xml │ │ │ │ ├── ic_couch_solid.xml │ │ │ │ ├── ic_credit_card.xml │ │ │ │ ├── ic_crown_solid.xml │ │ │ │ ├── ic_cubes_solid.xml │ │ │ │ ├── ic_cut_solid.xml │ │ │ │ ├── ic_desktop_solid.xml │ │ │ │ ├── ic_diaspora.xml │ │ │ │ ├── ic_dice_d6_solid.xml │ │ │ │ ├── ic_dna_solid.xml │ │ │ │ ├── ic_dog_solid.xml │ │ │ │ ├── ic_dollar_sign.xml │ │ │ │ ├── ic_dollar_sign_solid.xml │ │ │ │ ├── ic_dolly_flatbed_solid.xml │ │ │ │ ├── ic_dolly_solid.xml │ │ │ │ ├── ic_donate_solid.xml │ │ │ │ ├── ic_drafting_compass_solid.xml │ │ │ │ ├── ic_drum_solid.xml │ │ │ │ ├── ic_drumstick_bite_solid.xml │ │ │ │ ├── ic_dumbbell_solid.xml │ │ │ │ ├── ic_dumpster_solid.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_egg_solid.xml │ │ │ │ ├── ic_envelope.xml │ │ │ │ ├── ic_envelope_open.xml │ │ │ │ ├── ic_eraser_solid.xml │ │ │ │ ├── ic_euro_sign.xml │ │ │ │ ├── ic_euro_sign_solid.xml │ │ │ │ ├── ic_exchange_alt_solid.xml │ │ │ │ ├── ic_exclamation_circle_solid.xml │ │ │ │ ├── ic_exclamation_triangle_solid.xml │ │ │ │ ├── ic_expeditedssl.xml │ │ │ │ ├── ic_external_link_alt_solid.xml │ │ │ │ ├── ic_eye_dropper_solid.xml │ │ │ │ ├── ic_fan_solid.xml │ │ │ │ ├── ic_fax_solid.xml │ │ │ │ ├── ic_feather_alt_solid.xml │ │ │ │ ├── ic_female_solid.xml │ │ │ │ ├── ic_fighter_jet_solid.xml │ │ │ │ ├── ic_file.xml │ │ │ │ ├── ic_file_alt.xml │ │ │ │ ├── ic_file_audio.xml │ │ │ │ ├── ic_file_code.xml │ │ │ │ ├── ic_file_csv_solid.xml │ │ │ │ ├── ic_file_export_solid.xml │ │ │ │ ├── ic_file_import_solid.xml │ │ │ │ ├── ic_file_invoice_dollar_solid.xml │ │ │ │ ├── ic_file_invoice_solid.xml │ │ │ │ ├── ic_file_pdf.xml │ │ │ │ ├── ic_fill_solid.xml │ │ │ │ ├── ic_film_solid.xml │ │ │ │ ├── ic_fire_alt_solid.xml │ │ │ │ ├── ic_fire_extinguisher_solid.xml │ │ │ │ ├── ic_first_aid_solid.xml │ │ │ │ ├── ic_fish_solid.xml │ │ │ │ ├── ic_flag.xml │ │ │ │ ├── ic_flag_checkered_solid.xml │ │ │ │ ├── ic_flask_solid.xml │ │ │ │ ├── ic_fly.xml │ │ │ │ ├── ic_folder.xml │ │ │ │ ├── ic_football_ball_solid.xml │ │ │ │ ├── ic_fort_awesome.xml │ │ │ │ ├── ic_frown.xml │ │ │ │ ├── ic_futbol.xml │ │ │ │ ├── ic_gamepad_solid.xml │ │ │ │ ├── ic_gas_pump_solid.xml │ │ │ │ ├── ic_gavel_solid.xml │ │ │ │ ├── ic_gift_solid.xml │ │ │ │ ├── ic_glass_cheers_solid.xml │ │ │ │ ├── ic_glass_martini_alt_solid.xml │ │ │ │ ├── ic_globe_solid.xml │ │ │ │ ├── ic_golf_ball_solid.xml │ │ │ │ ├── ic_gopuram_solid.xml │ │ │ │ ├── ic_graduation_cap_solid.xml │ │ │ │ ├── ic_guitar_solid.xml │ │ │ │ ├── ic_hamburger_solid.xml │ │ │ │ ├── ic_hammer_solid.xml │ │ │ │ ├── ic_hat_cowboy_solid.xml │ │ │ │ ├── ic_hdd.xml │ │ │ │ ├── ic_headphones_solid.xml │ │ │ │ ├── ic_helicopter_solid.xml │ │ │ │ ├── ic_highlighter_solid.xml │ │ │ │ ├── ic_hiking_solid.xml │ │ │ │ ├── ic_home_solid.xml │ │ │ │ ├── ic_horse_head_solid.xml │ │ │ │ ├── ic_hospital.xml │ │ │ │ ├── ic_hotdog_solid.xml │ │ │ │ ├── ic_hourglass_half_solid.xml │ │ │ │ ├── ic_ice_cream_solid.xml │ │ │ │ ├── ic_id_card.xml │ │ │ │ ├── ic_image.xml │ │ │ │ ├── ic_inbox_solid.xml │ │ │ │ ├── ic_industry_solid.xml │ │ │ │ ├── ic_itunes_note.xml │ │ │ │ ├── ic_key_solid.xml │ │ │ │ ├── ic_keyboard.xml │ │ │ │ ├── ic_landmark_solid.xml │ │ │ │ ├── ic_laptop_solid.xml │ │ │ │ ├── ic_lightbulb.xml │ │ │ │ ├── ic_list_ul_solid.xml │ │ │ │ ├── ic_luggage_cart_solid.xml │ │ │ │ ├── ic_mail_bulk_solid.xml │ │ │ │ ├── ic_male_solid.xml │ │ │ │ ├── ic_map_marked_alt_solid.xml │ │ │ │ ├── ic_marker_solid.xml │ │ │ │ ├── ic_mars_solid.xml │ │ │ │ ├── ic_mask_solid.xml │ │ │ │ ├── ic_medal_solid.xml │ │ │ │ ├── ic_medapps.xml │ │ │ │ ├── ic_medkit_solid.xml │ │ │ │ ├── ic_mercury_solid.xml │ │ │ │ ├── ic_microchip_solid.xml │ │ │ │ ├── ic_microphone_alt_solid.xml │ │ │ │ ├── ic_microscope_solid.xml │ │ │ │ ├── ic_mobile_solid.xml │ │ │ │ ├── ic_money_bill_wave.xml │ │ │ │ ├── ic_money_check_alt_solid.xml │ │ │ │ ├── ic_mortar_pestle_solid.xml │ │ │ │ ├── ic_motorcycle_solid.xml │ │ │ │ ├── ic_mountain_solid.xml │ │ │ │ ├── ic_mug_hot_solid.xml │ │ │ │ ├── ic_oil_can_solid.xml │ │ │ │ ├── ic_pager_solid.xml │ │ │ │ ├── ic_paint_roller_solid.xml │ │ │ │ ├── ic_paperclip_solid.xml │ │ │ │ ├── ic_parachute_box_solid.xml │ │ │ │ ├── ic_parking_solid.xml │ │ │ │ ├── ic_passport_solid.xml │ │ │ │ ├── ic_paw_solid.xml │ │ │ │ ├── ic_pen_alt_solid.xml │ │ │ │ ├── ic_pen_solid.xml │ │ │ │ ├── ic_phone_solid.xml │ │ │ │ ├── ic_photo_video_solid.xml │ │ │ │ ├── ic_piggy_bank_solid.xml │ │ │ │ ├── ic_pills_solid.xml │ │ │ │ ├── ic_pizza_slice_solid.xml │ │ │ │ ├── ic_plane_solid.xml │ │ │ │ ├── ic_plug_solid.xml │ │ │ │ ├── ic_pound_sign.xml │ │ │ │ ├── ic_pound_sign_solid.xml │ │ │ │ ├── ic_prescription_bottle_solid.xml │ │ │ │ ├── ic_print_solid.xml │ │ │ │ ├── ic_question_circle.xml │ │ │ │ ├── ic_readme.xml │ │ │ │ ├── ic_recycle_solid.xml │ │ │ │ ├── ic_restroom_solid.xml │ │ │ │ ├── ic_road_solid.xml │ │ │ │ ├── ic_robot_solid.xml │ │ │ │ ├── ic_rocket_solid.xml │ │ │ │ ├── ic_running_solid.xml │ │ │ │ ├── ic_screwdriver_solid.xml │ │ │ │ ├── ic_scroll_solid.xml │ │ │ │ ├── ic_seedling_solid.xml │ │ │ │ ├── ic_server_solid.xml │ │ │ │ ├── ic_shield_alt_solid.xml │ │ │ │ ├── ic_ship_solid.xml │ │ │ │ ├── ic_shipping_fast_solid.xml │ │ │ │ ├── ic_shopping_bag_solid.xml │ │ │ │ ├── ic_shopping_cart_solid.xml │ │ │ │ ├── ic_shuttle_van_solid.xml │ │ │ │ ├── ic_signal_solid.xml │ │ │ │ ├── ic_sim_card_solid.xml │ │ │ │ ├── ic_skating_solid.xml │ │ │ │ ├── ic_skiing_nordic_solid.xml │ │ │ │ ├── ic_skiing_solid.xml │ │ │ │ ├── ic_smoking_solid.xml │ │ │ │ ├── ic_sms_solid.xml │ │ │ │ ├── ic_snowboarding_solid.xml │ │ │ │ ├── ic_snowflake.xml │ │ │ │ ├── ic_socks_solid.xml │ │ │ │ ├── ic_spider_solid.xml │ │ │ │ ├── ic_spray_can_solid.xml │ │ │ │ ├── ic_stamp_solid.xml │ │ │ │ ├── ic_star_of_life_solid.xml │ │ │ │ ├── ic_stethoscope_solid.xml │ │ │ │ ├── ic_sticky_note.xml │ │ │ │ ├── ic_stopwatch_solid.xml │ │ │ │ ├── ic_store_alt_solid.xml │ │ │ │ ├── ic_subway_solid.xml │ │ │ │ ├── ic_suitcase_solid.xml │ │ │ │ ├── ic_swimmer_solid.xml │ │ │ │ ├── ic_syringe_solid.xml │ │ │ │ ├── ic_table_tennis_solid.xml │ │ │ │ ├── ic_tablet_solid.xml │ │ │ │ ├── ic_tachometer_alt_solid.xml │ │ │ │ ├── ic_tag_solid.xml │ │ │ │ ├── ic_taxi_solid.xml │ │ │ │ ├── ic_temperature_high_solid.xml │ │ │ │ ├── ic_terminal_solid.xml │ │ │ │ ├── ic_theater_masks_solid.xml │ │ │ │ ├── ic_thermometer_full_solid.xml │ │ │ │ ├── ic_ticket_alt_solid.xml │ │ │ │ ├── ic_tint_solid.xml │ │ │ │ ├── ic_toilet_paper_solid.xml │ │ │ │ ├── ic_toolbox_solid.xml │ │ │ │ ├── ic_tools_solid.xml │ │ │ │ ├── ic_tooth_solid.xml │ │ │ │ ├── ic_tractor_solid.xml │ │ │ │ ├── ic_train_solid.xml │ │ │ │ ├── ic_trash_alt.xml │ │ │ │ ├── ic_tree_solid.xml │ │ │ │ ├── ic_trophy_solid.xml │ │ │ │ ├── ic_truck_loading_solid.xml │ │ │ │ ├── ic_truck_moving_solid.xml │ │ │ │ ├── ic_truck_pickup_solid.xml │ │ │ │ ├── ic_tshirt_solid.xml │ │ │ │ ├── ic_tv_solid.xml │ │ │ │ ├── ic_university_solid.xml │ │ │ │ ├── ic_user.xml │ │ │ │ ├── ic_user_friends_solid.xml │ │ │ │ ├── ic_utensils_solid.xml │ │ │ │ ├── ic_venus_solid.xml │ │ │ │ ├── ic_vial_solid.xml │ │ │ │ ├── ic_video_solid.xml │ │ │ │ ├── ic_volleyball_ball_solid.xml │ │ │ │ ├── ic_volume_up_solid.xml │ │ │ │ ├── ic_walking_solid.xml │ │ │ │ ├── ic_wallet_solid.xml │ │ │ │ ├── ic_wine_glass_solid.xml │ │ │ │ ├── ic_wrench_solid.xml │ │ │ │ └── ic_yen_sign_solid.xml │ │ │ └── values/ │ │ │ └── strings.xml │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── prof18/ │ │ │ └── moneyflow/ │ │ │ ├── MainViewModel.kt │ │ │ ├── data/ │ │ │ │ ├── MoneyRepository.kt │ │ │ │ ├── SettingsRepository.kt │ │ │ │ └── settings/ │ │ │ │ └── SettingsSource.kt │ │ │ ├── database/ │ │ │ │ ├── DatabaseHelper.kt │ │ │ │ ├── default/ │ │ │ │ │ └── DefaultValues.kt │ │ │ │ └── model/ │ │ │ │ └── TransactionType.kt │ │ │ ├── di/ │ │ │ │ └── Koin.kt │ │ │ ├── domain/ │ │ │ │ └── entities/ │ │ │ │ ├── BalanceRecap.kt │ │ │ │ ├── Category.kt │ │ │ │ ├── CurrencyConfig.kt │ │ │ │ ├── DBImportExportException.kt │ │ │ │ ├── MoneyFlowError.kt │ │ │ │ ├── MoneyFlowResult.kt │ │ │ │ ├── MoneySummary.kt │ │ │ │ ├── MoneyTransaction.kt │ │ │ │ └── TransactionTypeUI.kt │ │ │ ├── features/ │ │ │ │ ├── addtransaction/ │ │ │ │ │ └── AddTransactionViewModel.kt │ │ │ │ ├── alltransactions/ │ │ │ │ │ └── AllTransactionsViewModel.kt │ │ │ │ ├── authentication/ │ │ │ │ │ └── BiometricAuthenticator.kt │ │ │ │ ├── categories/ │ │ │ │ │ └── CategoriesViewModel.kt │ │ │ │ ├── home/ │ │ │ │ │ └── HomeViewModel.kt │ │ │ │ └── settings/ │ │ │ │ ├── BiometricAvailabilityChecker.kt │ │ │ │ └── SettingsViewModel.kt │ │ │ ├── navigation/ │ │ │ │ ├── AppRoute.kt │ │ │ │ └── MoneyFlowNavHost.kt │ │ │ ├── presentation/ │ │ │ │ ├── MoneyFlowApp.kt │ │ │ │ ├── MoneyFlowErrorMapper.kt │ │ │ │ ├── addtransaction/ │ │ │ │ │ ├── AddTransactionAction.kt │ │ │ │ │ ├── AddTransactionScreen.kt │ │ │ │ │ ├── TransactionToSave.kt │ │ │ │ │ └── components/ │ │ │ │ │ ├── IconTextClicableRow.kt │ │ │ │ │ ├── MFTextField.kt │ │ │ │ │ └── TransactionTypeTabBar.kt │ │ │ │ ├── alltransactions/ │ │ │ │ │ └── AllTransactionsScreen.kt │ │ │ │ ├── auth/ │ │ │ │ │ ├── AuthInProgressScreen.kt │ │ │ │ │ └── AuthState.kt │ │ │ │ ├── budget/ │ │ │ │ │ └── BudgetScreen.kt │ │ │ │ ├── categories/ │ │ │ │ │ ├── CategoriesScreen.kt │ │ │ │ │ ├── CategoryModel.kt │ │ │ │ │ ├── IconCategoryMapper.kt │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── CategoryCard.kt │ │ │ │ │ └── data/ │ │ │ │ │ └── CategoryUIData.kt │ │ │ │ ├── home/ │ │ │ │ │ ├── HomeModel.kt │ │ │ │ │ ├── HomeScreen.kt │ │ │ │ │ └── components/ │ │ │ │ │ ├── HeaderNavigator.kt │ │ │ │ │ └── HomeRecap.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── CategoryIcon.kt │ │ │ │ │ ├── UIErrorMessage.kt │ │ │ │ │ └── UIErrorMessageFactory.kt │ │ │ │ ├── recap/ │ │ │ │ │ └── RecapScreen.kt │ │ │ │ └── settings/ │ │ │ │ └── SettingsScreen.kt │ │ │ ├── ui/ │ │ │ │ ├── components/ │ │ │ │ │ ├── ArrowCircleIcon.kt │ │ │ │ │ ├── ErrorView.kt │ │ │ │ │ ├── HideableTextField.kt │ │ │ │ │ ├── Loader.kt │ │ │ │ │ ├── MFTopBar.kt │ │ │ │ │ ├── SwitchWithText.kt │ │ │ │ │ └── TransactionCard.kt │ │ │ │ └── style/ │ │ │ │ ├── Color.kt │ │ │ │ ├── Margins.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Typography.kt │ │ │ └── utils/ │ │ │ ├── CurrencyFormatter.kt │ │ │ ├── DispatcherProvider.kt │ │ │ ├── LocalAppDensity.kt │ │ │ ├── LocalAppLocale.kt │ │ │ ├── LocalAppTheme.kt │ │ │ └── Utils.kt │ │ └── sqldelight/ │ │ └── com/ │ │ └── prof18/ │ │ └── moneyflow/ │ │ └── db/ │ │ ├── AccountTable.sq │ │ ├── CategoryTable.sq │ │ └── TransactionTable.sq │ ├── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── prof18/ │ │ └── moneyflow/ │ │ └── utilities/ │ │ └── TestDatabaseHelper.kt │ ├── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── prof18/ │ │ └── moneyflow/ │ │ ├── IosBiometricAuthenticator.kt │ │ ├── IosBiometricAvailabilityChecker.kt │ │ ├── MainViewController.kt │ │ ├── database/ │ │ │ └── DatabaseDriverFactory.kt │ │ ├── di/ │ │ │ └── KoinIos.kt │ │ └── utils/ │ │ ├── LocalAppLocale.ios.kt │ │ └── LocalAppTheme.ios.kt │ └── iosTest/ │ └── kotlin/ │ └── com/ │ └── prof18/ │ └── moneyflow/ │ └── utilities/ │ ├── DatabaseHelperIosTest.kt │ └── TestUtilsIos.kt ├── version.properties └── versioning.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/settings.local.json ================================================ { "permissions": { "allow": [ "Bash(lipo -info:*)", "Bash(./gradlew:*)", "Bash(find:*)", "Bash(wc:*)", "Bash(cat:*)" ], "deny": [], "ask": [] } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms custom: https://www.paypal.me/MarcoGomiero ================================================ FILE: .github/actions/setup-gradle/action.yml ================================================ name: Setup Environment description: Setup the environment (Java and Gradle) inputs: gradle-cache-encryption-key: description: 'The encryption key to use for the Gradle cache' required: true runs: using: 'composite' steps: - name: set up JDK uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - uses: gradle/actions/setup-gradle@v5 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }} - name: Setup Gradle Properties shell: bash run: | echo "org.gradle.jvmargs=-Xms10g -Xmx10g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC" >> ~/.gradle/gradle.properties echo "kotlin.daemon.jvmargs=-Xms4g -Xmx4g -XX:+UseParallelGC" >> ~/.gradle/gradle.properties echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties echo "org.gradle.workers.max=2" >> ~/.gradle/gradle.properties echo "org.gradle.vfs.watch=false" >> ~/.gradle/gradle.properties - name: Cache KMP tooling uses: actions/cache@v4 with: path: | ~/.konan key: ${{ runner.os }}-v1-${{ hashFiles('**/libs.versions.toml') }} ================================================ FILE: .github/workflows/android-release.yml ================================================ name: Android Release on: workflow_call: secrets: GRADLE_CACHE_ENCRYPTION_KEY: required: true KEYSTORE_FILE: required: true KEYSTORE_PASSPHRASE: required: true KEYSTORE_KEY_ALIAS: required: true KEYSTORE_KEY_PASSWORD: required: true KEYSTORE_STORE_PASSWORD: required: true PLAY_CONFIG: required: true jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Configure Keystore run: | echo '${{ secrets.KEYSTORE_FILE }}' > release.keystore.asc gpg -d --passphrase '${{ secrets.KEYSTORE_PASSPHRASE }}' --batch release.keystore.asc > androidApp/release.keystore echo "storeFile=release.keystore" >> keystore.properties echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties env: KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} - name: Create Google Play Config file run: | echo "$PLAY_CONFIG_JSON" > play_config.json.b64 base64 -d -i play_config.json.b64 > play_config.json env: PLAY_CONFIG_JSON: ${{ secrets.PLAY_CONFIG }} - name: Distribute app to Alpha track run: ./gradlew :androidApp:bundleRelease :androidApp:publishReleaseBundle ================================================ FILE: .github/workflows/checks.yml ================================================ name: Code Checks on: push: branches: - '*' pull_request: branches: - '*' jobs: checks: runs-on: macos-15 timeout-minutes: 30 steps: - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Run Checks run: ./gradlew check - name: Upload reports if: failure() uses: actions/upload-artifact@v5 with: name: build-reports path: | **/build/reports/* build-android-app: name: Build Android App runs-on: ubuntu-latest needs: [ checks ] steps: - uses: actions/checkout@v6 - name: Setup environment uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Build Android Sample run: ./gradlew :androidApp:assembleDebug build-ios-app: name: Build iOS App runs-on: macos-15 needs: [ checks ] steps: - uses: actions/checkout@v6 - name: Xcode version run: | /usr/bin/xcodebuild -version - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Xcode version run: | /usr/bin/xcodebuild -version - name: Setup environment uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Build iOS Sample run: | cd iosApp xcodebuild -configuration Debug -scheme MoneyFlow -sdk iphoneos -destination name='iPhone 17 Pro' build | xcbeautify --renderer github-actions ================================================ FILE: .github/workflows/ios-release.yml ================================================ name: iOS Release on: workflow_call: secrets: GRADLE_CACHE_ENCRYPTION_KEY: required: true CERTIFICATES_P12: required: true CERTIFICATES_PASSWORD: required: true BUNDLE_ID: required: true APPSTORE_ISSUER_ID: required: true APPSTORE_KEY_ID: required: true APPSTORE_PRIVATE_KEY: required: true APPSTORE_TEAM_ID: required: true jobs: deploy: runs-on: macos-15 timeout-minutes: 90 steps: - uses: actions/checkout@v6 - name: Xcode version run: | /usr/bin/xcodebuild -version - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Xcode version run: | /usr/bin/xcodebuild -version - name: Setup Gradle uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: import certs uses: apple-actions/import-codesign-certs@v5 with: p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} p12-password: ${{ secrets.CERTIFICATES_PASSWORD }} - name: download provisioning profiles uses: apple-actions/download-provisioning-profiles@v4 with: bundle-id: ${{ secrets.BUNDLE_ID }} issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} - name: build archive run: | cd iosApp xcrun xcodebuild \ -scheme "MoneyFlow" \ -configuration "Release" \ -sdk "iphoneos" \ -showBuildTimingSummary \ -disableAutomaticPackageResolution \ -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" \ -archivePath "${RUNNER_TEMP}/Build/Archives/MoneyFlow.xcarchive" \ -resultBundlePath "${RUNNER_TEMP}/Build/Artifacts/MoneyFlow.xcresult" \ -destination "generic/platform=iOS" \ DEVELOPMENT_TEAM="${{ secrets.APPSTORE_TEAM_ID }}" \ CODE_SIGN_STYLE="Manual" \ archive | xcbeautify --renderer github-actions - name: "Generate ExportOptions.plist" run: | cat < ${RUNNER_TEMP}/Build/ExportOptions.plist destination export method app-store signingStyle manual generateAppStoreInformation stripSwiftSymbols teamID ${{ secrets.APPSTORE_TEAM_ID }} uploadSymbols provisioningProfiles ${{ secrets.BUNDLE_ID }} MoneyFlowGHActionDistributionProvisioning EOF - id: export_archive name: export archive run: | xcrun xcodebuild \ -exportArchive \ -exportOptionsPlist "${RUNNER_TEMP}/Build/ExportOptions.plist" \ -archivePath "${RUNNER_TEMP}/Build/Archives/MoneyFlow.xcarchive" \ -exportPath "${RUNNER_TEMP}/Build/Archives/MoneyFlow.xcarchive" \ PRODUCT_BUNDLE_IDENTIFIER="${{ secrets.BUNDLE_ID }}" | xcbeautify --renderer github-actions echo "ipa_path=${RUNNER_TEMP}/Build/Archives/MoneyFlow.xcarchive/MoneyFlow.ipa" >> $GITHUB_ENV - uses: Apple-Actions/upload-testflight-build@v3 with: app-path: ${{ env.ipa_path }} issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: inputs: platforms: description: 'Platforms to build for prerelease' required: true type: choice options: - all - android - ios default: 'all' pull_request_target: types: - labeled push: tags: - '*-all' - '*-android' - '*-ios' jobs: android: if: (github.event_name == 'workflow_dispatch' && (github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'android')) || (github.event_name == 'push' && (endsWith(github.ref, '-all') || endsWith(github.ref, '-android'))) || (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'Generate Build') uses: ./.github/workflows/android-release.yml secrets: GRADLE_CACHE_ENCRYPTION_KEY: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} KEYSTORE_PASSPHRASE: ${{ secrets.KEYSTORE_PASSPHRASE }} KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} PLAY_CONFIG: ${{ secrets.PLAY_CONFIG_JSON }} ios: if: (github.event_name == 'workflow_dispatch' && (github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'ios')) || (github.event_name == 'push' && (endsWith(github.ref, '-all') || endsWith(github.ref, '-ios'))) || (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'Generate Build') uses: ./.github/workflows/ios-release.yml secrets: GRADLE_CACHE_ENCRYPTION_KEY: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }} CERTIFICATES_PASSWORD: ${{ secrets.CERTIFICATES_PASSWORD }} BUNDLE_ID: ${{ secrets.BUNDLE_ID }} APPSTORE_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }} APPSTORE_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }} APPSTORE_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }} APPSTORE_TEAM_ID: ${{ secrets.APPSTORE_TEAM_ID }} ================================================ FILE: .github/workflows/roborazzi.yml ================================================ name: Roborazzi Snapshots on: pull_request: branches: - '*' permissions: contents: write jobs: record-snapshots: name: Record Roborazzi Snapshots runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup environment uses: ./.github/actions/setup-gradle with: gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Record Roborazzi snapshots run: ./gradlew recordRoborazziDebug - name: Commit Roborazzi snapshots if: always() && !github.event.pull_request.head.repo.fork run: | BRANCH="${{ github.event.pull_request.head.ref }}" if git status --porcelain image/roborazzi | grep .; then git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add image/roborazzi git commit -m "chore: update Roborazzi snapshots" git push origin HEAD:"$BRANCH" else echo "No snapshot changes to commit." fi ================================================ FILE: .gitignore ================================================ # Dependencies node_modules/ .pnp/ .pnp.js # Build outputs dist/ build/ *.tsbuildinfo reader-playground/frontend/.vite/ # Environment files .env .env.local .env.development.local .env.test.local .env.production.local # IDE and editor files .idea/ .vscode/ *.swp *.swo .DS_Store # Logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Testing coverage/ # Temporary files tmp/ temp/ .idea .DS_Store build captures .externalNativeBuild .cxx xcuserdata Plist # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Log/OS Files *.log # Android Studio generated files and folders captures/ .externalNativeBuild/ .cxx/ *.apk output.json # IntelliJ *.iml .idea/ misc.xml deploymentTargetDropDown.xml render.experimental.xml # Keystore files *.jks *.keystore keystore.properties # Google Services (e.g. APIs or Firebase) google-services.json GoogleService-Info.plist # Android Profiling *.hprof # Compiled class file *.class # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar *.aab # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* !/gradle/wrapper/gradle-wrapper.jar /fastlane/report.xml /fastlane/README.md .gradle !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ ### IntelliJ IDEA ### .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml .idea/libraries/ *.iws *.ipr out/ !**/src/main/**/out/ !**/src/test/**/out/ ### Eclipse ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ /iosApp/Plist/Prefix /iosApp/GoogleService-Info.plist /iosApp/GoogleService-Info-dev.plist /iosApp/Assets/Config.xcconfig /desktopApp/src/jvmMain/resources/props.properties /desktopApp/embedded.provisionprofile /desktopApp/runtime.provisionprofile /website/public/ /.kotlin/ /play_config.json /iosApp/buildServer.json /build-logic/.kotlin/ # xcode-build-server files buildServer.json .compile # Local build artifacts .build/ ================================================ FILE: AGENTS.md ================================================ # AGENTS.md ## Project Overview MoneyFlow is a Kotlin Multiplatform (KMP) personal finance app targeting Android and iOS with Compose Multiplatform. The app is actively being rewritten to keep Android/iOS shells thin while concentrating UI and business logic in the shared module. ## Project Structure & Module Organization ### Module Structure - `shared/`: All KMP code including Compose UI - `androidApp/`: Android launcher shell (minimal) - `iosApp/`: Xcode project wrapper (minimal) - `build-logic/`: Gradle convention plugins - `config/`: Detekt and quality configurations ## Build, Test, and Development Commands ### Build Commands All Gradle commands should use `-q --console=plain` for readable output. - `./gradlew :androidApp:assembleDebug` -> Build Android app - `./gradlew test` -> Run all tests for Android & iOS - `./gradlew detekt` -> Run static analysis with Detekt for Shared code, Android and Desktop - `./gradlew recordRoborazziDebug` -> Record new snapshots ### Building for iOS Simulator To build MoneyFlow for iPhone 17 Pro simulator: ```bash mcp__XcodeBuildMCP__build_sim_name_proj projectPath: "/Users/mg/Workspace/MoneyFlow/iosApp/MoneyFlow.xcodeproj" scheme: "MoneyFlow" simulatorName: "iPhone 17 Pro" ``` There could be different project path on your machine. Always use the first one. The alternative paths will be: ```bash mcp__XcodeBuildMCP__build_sim_name_proj projectPath: "/Users/mg/Workspace/tmp/MoneyFlow/iosApp/MoneyFlow.xcodeproj" scheme: "MoneyFlow" simulatorName: "iPhone 17 Pro" ``` To launch MoneyFlow for iPhone 17 Pro simulator: ```bash mcp__XcodeBuildMCP__launch_app_sim projectPath: "/Users/mg/Workspace/MoneyFlow/iosApp/MoneyFlow.xcodeproj" scheme: "MoneyFlow" simulatorName: "iPhone 17 Pro" ``` ### Build Verification Process IMPORTANT: When editing code, you MUST: 1. Build the project after making changes 2. Fix any compilation errors before proceeding Be sure to build ONLY for the platform you are working on to save time. ## Handing off Before handing off you must run `./gradlew detekt` to ensure all checks pass - don't run it if you modified only swift files ## General rules: - DO NOT write comments for every function or class. Only write comments when the code is not self-explanatory. - DO NOT write tests unless specifically told to do so. - DO NOT excessively use try/catch blocks for every function. Use them only for the top caller or the bottom callers, depending on the cases. - ALWAYS run gradle tasks with the following flag: `--quiet --console=plain` ### Git Commit Messages When creating commits: - Use simple, one-liner commit messages - DO NOT include phase numbers (e.g., "Phase 1", "Phase 2") - DO NOT add "Generated with Claude Code" attribution - DO NOT add "Co-Authored-By: Claude" attribution - Example: `git commit -m "Add foundation for unified article parsing system"` ## Testing - Unit tests: `shared/src/commonTest/` (cross-platform), `androidUnitTest/`, `iosTest/` - Screenshot tests: Use Roborazzi, run `recordRoborazziDebug` to update snapshots - When modifying UI composables, search for related tests and update snapshots ## CI/CD Notes - `./setup.sh` installs Android SDK if you are an agent running on a sandbox environment (Codex Cloud, Claude Code Web). DO NOT run it on local-machine. ================================================ FILE: README.md ================================================ # MoneyFlow MoneyFlow is a Kotlin Multiplatform personal finance app targeting Android and iOS with Compose Multiplatform.
## Current direction - The app is in the middle of a rewrite with an updated architecture to better support Compose Multiplatform and keep the Android/iOS shells thin. - Expect fast-moving changes while core features are rebuilt. - The shared module remains the source of truth, and platform-specific code is being pared back to minimal glue. ## Project history - MoneyFlow originally explored several shared-architecture approaches for KMP + Jetpack Compose + SwiftUI; the ongoing rewrite changes direction to simplify the stack and improve maintainability. - Legacy articles documenting the earlier architecture remain for context: - [Improving shared architecture for a Kotlin Multiplatform, Jetpack Compose and SwiftUI app](https://www.marcogomiero.com/posts/2022/improved-kmm-shared-app-arch/) - [Choosing the right architecture for a [new] Kotlin Multiplatform, Jetpack Compose and SwiftUI app](https://www.marcogomiero.com/posts/2020/kmm-shared-app-architecture/) ## Feature roadmap - ✅ Transaction Entry - 🏗 Transaction List - 💭 Edit Transaction - 💭 Add custom category - 💭 Recap screen with plots - 💭 Budgeting feature - 💭 Import from CSV - 💭 CSV data export - 💭 Change currency - 🏗 Lock view with biometrics Legend: - ✅ Implemented - 💭 Not yet implemented - 🏗 In progress ## License 📄 ``` Copyright 2020-2022 Marco Gomiero Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: androidApp/build.gradle.kts ================================================ import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.compose.compiler) alias(libs.plugins.triplet.play) alias(libs.plugins.kmp.detekt) } apply(from = "../versioning.gradle.kts") val appVersionCode: () -> Int by extra val appVersionName: () -> String by extra val local = Properties() val localProperties: File = rootProject.file("keystore.properties") if (localProperties.exists()) { localProperties.inputStream().use { local.load(it) } } val appVersionCode: String by project val appVersionName: String by project val javaVersion: JavaVersion by rootProject.extra android { namespace = "com.prof18.moneyflow.androidApp" compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { applicationId = "com.prof18.moneyflow" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = appVersionCode() versionName = appVersionName() } signingConfigs { create("release") { keyAlias = local.getProperty("keyAlias") keyPassword = local.getProperty("keyPassword") storeFile = file(local.getProperty("storeFile") ?: "NOT_FOUND") storePassword = local.getProperty("storePassword") } } buildTypes { getByName("debug") { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" } getByName("release") { isMinifyEnabled = true isShrinkResources = true signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { sourceCompatibility = javaVersion targetCompatibility = javaVersion } buildFeatures { compose = true buildConfig = true } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } kotlin { jvmToolchain(21) } dependencies { implementation(project(":shared")) implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.koin.android) implementation(libs.androidx.biometric.ktx) debugImplementation(libs.compose.ui.tooling) } play { // The play_config.json file will be provided on CI serviceAccountCredentials.set(file("../play_config.json")) track.set("internal") } ================================================ FILE: androidApp/proguard-rules.pro ================================================ # Okhttp # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** # A resource is loaded with a relative path so the package of this class must be preserved. -adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* # OkHttp platform used only on JVM and when Conscrypt and other security providers are available. -dontwarn okhttp3.internal.platform.** -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** # Okio # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* -dontwarn org.slf4j.impl.StaticLoggerBinder # Keep LinkOpeningPreference enum and its serialization -keepclassmembers class * { @kotlinx.serialization.Serializable ; } # Keep the DateTimeComponents class and all its members # TODO: remove when https://github.com/Kotlin/kotlinx-datetime/issues/519 is closed -keep class kotlinx.datetime.format.DateTimeComponents { *; } ================================================ FILE: androidApp/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: androidApp/src/main/AndroidManifest.xml ================================================ ================================================ FILE: androidApp/src/main/kotlin/com/prof18/moneyflow/MainActivity.kt ================================================ package com.prof18.moneyflow import android.content.res.Configuration import android.graphics.Color import android.os.Bundle import android.view.WindowManager import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.fragment.app.FragmentActivity import com.prof18.moneyflow.presentation.MoneyFlowApp import org.koin.androidx.viewmodel.ext.android.viewModel class MainActivity : FragmentActivity() { private val viewModel: MainViewModel by viewModel() private val biometricAuthenticator by lazy { AndroidBiometricAuthenticator(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val isDarkMode = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( lightScrim = Color.TRANSPARENT, darkScrim = Color.TRANSPARENT, ) { isDarkMode }, navigationBarStyle = SystemBarStyle.auto( lightScrim = Color.TRANSPARENT, darkScrim = Color.TRANSPARENT, ) { isDarkMode }, ) window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE, ) setContent { MoneyFlowApp( biometricAuthenticator = biometricAuthenticator, ) } } override fun onResume() { super.onResume() performAuth() } override fun onStop() { super.onStop() viewModel.lockIfNeeded(biometricAuthenticator) } private fun performAuth() { viewModel.performAuthentication(biometricAuthenticator) } } ================================================ FILE: androidApp/src/main/kotlin/com/prof18/moneyflow/MoneyFlowApp.kt ================================================ package com.prof18.moneyflow import android.app.Application import android.content.Context import com.prof18.moneyflow.di.initKoin import org.koin.dsl.module class MoneyFlowApp : Application() { override fun onCreate() { super.onCreate() // TODO: do proper logging setup like FeedFlow // Logger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Verbose else Severity.Warn) initKoin( listOf( module { single { this@MoneyFlowApp } }, ), ) } } ================================================ FILE: androidApp/src/release/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: build-logic/convention/build.gradle.kts ================================================ plugins { `kotlin-dsl` } java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } dependencies { compileOnly(libs.detekt.gradle) compileOnly(libs.detekt.formatting) } gradlePlugin { plugins { register("detekt") { id = "com.moneyflow.detekt" implementationClass = "DetektConventionPlugin" } } } ================================================ FILE: build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt ================================================ import io.gitlab.arturbosch.detekt.extensions.DetektExtension import io.gitlab.arturbosch.detekt.extensions.DetektExtension.Companion.DEFAULT_SRC_DIR_JAVA import io.gitlab.arturbosch.detekt.extensions.DetektExtension.Companion.DEFAULT_SRC_DIR_KOTLIN import io.gitlab.arturbosch.detekt.extensions.DetektExtension.Companion.DEFAULT_TEST_SRC_DIR_JAVA import io.gitlab.arturbosch.detekt.extensions.DetektExtension.Companion.DEFAULT_TEST_SRC_DIR_KOTLIN import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType class DetektConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { apply("io.gitlab.arturbosch.detekt") } val libs = extensions.getByType().named("libs") extensions.configure { config.setFrom("$rootDir/config/detekt/detekt.yml") parallel = true source.setFrom( files( "src", DEFAULT_SRC_DIR_JAVA, DEFAULT_TEST_SRC_DIR_JAVA, DEFAULT_SRC_DIR_KOTLIN, DEFAULT_TEST_SRC_DIR_KOTLIN, ), ) dependencies { add("detektPlugins", libs.findLibrary("detekt-formatting").get()) add("detektPlugins", libs.findLibrary("detekt-compose-rules").get()) } } } } } ================================================ FILE: build-logic/settings.gradle.kts ================================================ pluginManagement { repositories { google() gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositories { google() mavenCentral() } versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } rootProject.name = "build-logic" include(":convention") ================================================ FILE: build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.detekt) apply false alias(libs.plugins.sqldelight) apply false alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.triplet.play) apply false } val javaVersion by extra { JavaVersion.VERSION_21 } ================================================ FILE: config/detekt/detekt.yml ================================================ build: maxIssues: 0 excludeCorrectable: false weights: # complexity: 2 # LongParameterList: 1 # style: 1 # comments: 1 config: validation: true warningsAsErrors: false checkExhaustiveness: false # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' excludes: '' processors: active: true exclude: - 'DetektProgressListener' # - 'KtFileCountProcessor' # - 'PackageCountProcessor' # - 'ClassCountProcessor' # - 'FunctionCountProcessor' # - 'PropertyCountProcessor' # - 'ProjectComplexityProcessor' # - 'ProjectCognitiveComplexityProcessor' # - 'ProjectLLOCProcessor' # - 'ProjectCLOCProcessor' # - 'ProjectLOCProcessor' # - 'ProjectSLOCProcessor' # - 'LicenseHeaderLoaderExtension' console-reports: active: true exclude: - 'ProjectStatisticsReport' - 'ComplexityReport' - 'NotificationReport' - 'FindingsReport' - 'FileBasedFindingsReport' # - 'LiteFindingsReport' output-reports: active: true # exclude: # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' # - 'MdOutputReport' # - 'SarifOutputReport' comments: active: true AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template' licenseTemplateIsRegex: false CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false DeprecatedBlockTag: active: false EndOfSentenceFormat: active: false endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' KDocReferencesNonPublicProperty: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] OutdatedDocumentation: active: false matchTypeParameters: true matchDeclarationsOrder: true allowParamOnConstructorProperties: false UndocumentedPublicClass: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] searchInNestedClass: true searchInInnerClass: true searchInInnerObject: true searchInInnerInterface: true searchInProtectedClass: false UndocumentedPublicFunction: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] searchProtectedFunction: false UndocumentedPublicProperty: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] searchProtectedProperty: false complexity: active: true CognitiveComplexMethod: active: false threshold: 15 ComplexCondition: active: true threshold: 5 ComplexInterface: active: false threshold: 10 includeStaticDeclarations: false includePrivateDeclarations: false ignoreOverloaded: false CyclomaticComplexMethod: active: true threshold: 16 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false nestingFunctions: - 'also' - 'apply' - 'forEach' - 'isNotNull' - 'ifNull' - 'let' - 'run' - 'use' - 'with' LabeledExpression: active: false ignoredLabels: [] LargeClass: active: false threshold: 600 LongMethod: active: false threshold: 60 LongParameterList: active: false functionThreshold: 10 constructorThreshold: 7 ignoreDefaultParameters: true ignoreDataClasses: true ignoreAnnotatedParameter: [] MethodOverloading: active: false threshold: 6 NamedArguments: active: false threshold: 3 ignoreArgumentsMatchingNames: false NestedBlockDepth: active: true threshold: 4 NestedScopeFunctions: active: false threshold: 1 functions: - 'kotlin.apply' - 'kotlin.run' - 'kotlin.with' - 'kotlin.let' - 'kotlin.also' ReplaceSafeCallChainWithRun: active: false StringLiteralDuplication: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] thresholdInFiles: 12 thresholdInClasses: 12 thresholdInInterfaces: 12 thresholdInObjects: 12 thresholdInEnums: 12 ignoreDeprecated: false ignorePrivate: false ignoreOverridden: false coroutines: active: true GlobalCoroutineUsage: active: false InjectDispatcher: active: true dispatcherNames: - 'IO' - 'Default' - 'Unconfined' RedundantSuspendModifier: active: true SleepInsteadOfDelay: active: true SuspendFunSwallowedCancellation: active: false SuspendFunWithCoroutineScopeReceiver: active: false SuspendFunWithFlowReturnType: active: true empty-blocks: active: true EmptyCatchBlock: active: true allowedExceptionNameRegex: '_|(ignore|expected).*' EmptyClassBlock: active: true EmptyDefaultConstructor: active: true EmptyDoWhileBlock: active: true EmptyElseBlock: active: true EmptyFinallyBlock: active: true EmptyForBlock: active: true EmptyFunctionBlock: active: true ignoreOverridden: false EmptyIfBlock: active: true EmptyInitBlock: active: true EmptyKtFile: active: true EmptySecondaryConstructor: active: true EmptyTryBlock: active: true EmptyWhenBlock: active: true EmptyWhileBlock: active: true exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true methodNames: - 'equals' - 'finalize' - 'hashCode' - 'toString' InstanceOfCheckForException: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] NotImplementedDeclaration: active: false ObjectExtendsThrowable: active: false PrintStackTrace: active: true RethrowCaughtException: active: true ReturnFromFinally: active: true ignoreLabeled: false SwallowedException: active: true ignoredExceptionTypes: - 'InterruptedException' - 'MalformedURLException' - 'NumberFormatException' - 'ParseException' allowedExceptionNameRegex: '_|(ignore|expected).*' ThrowingExceptionFromFinally: active: true ThrowingExceptionInMain: active: false ThrowingExceptionsWithoutMessageOrCause: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] exceptions: - 'ArrayIndexOutOfBoundsException' - 'Exception' - 'IllegalArgumentException' - 'IllegalMonitorStateException' - 'IllegalStateException' - 'IndexOutOfBoundsException' - 'NullPointerException' - 'RuntimeException' - 'Throwable' ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] exceptionNames: - 'ArrayIndexOutOfBoundsException' - 'Error' - 'Exception' - 'IllegalMonitorStateException' - 'IndexOutOfBoundsException' - 'NullPointerException' - 'RuntimeException' - 'Throwable' allowedExceptionNameRegex: '_|(ignore|expected).*' TooGenericExceptionThrown: active: true exceptionNames: - 'Error' - 'Exception' - 'RuntimeException' - 'Throwable' naming: active: true BooleanPropertyNaming: active: false allowedPattern: '^(is|has|are)' ClassNaming: active: true classPattern: '[A-Z][a-zA-Z0-9]*' ConstructorParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' EnumNaming: active: true enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false forbiddenName: [] FunctionMaxLength: active: false maximumFunctionNameLength: 30 FunctionMinLength: active: false minimumFunctionNameLength: 3 FunctionNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' ignoreAnnotated: ['Composable'] FunctionParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' InvalidPackageDeclaration: active: true rootPackage: '' requireRootInDeclaration: false LambdaParameterNaming: active: false parameterPattern: '[a-z][A-Za-z0-9]*|_' MatchingDeclarationName: active: false mustBeFirst: true MemberNameEqualsClassName: active: true ignoreOverridden: true NoNameShadowing: active: true NonBooleanPropertyPrefixedWithIs: active: false ObjectPropertyNaming: active: true constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' TopLevelPropertyNaming: active: true constantPattern: '[A-Z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false maximumVariableNameLength: 64 VariableMinLength: active: false minimumVariableNameLength: 1 VariableNaming: active: true variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' performance: active: true ArrayPrimitive: active: true CouldBeSequence: active: false threshold: 3 ForEachOnRange: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] SpreadOperator: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] UnnecessaryPartOfBinaryExpression: active: false UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true AvoidReferentialEquality: active: true forbiddenTypePatterns: - 'kotlin.String' CastNullableToNonNullableType: active: false CastToNullableType: active: false Deprecation: active: false DontDowncastCollectionTypes: active: false DoubleMutabilityForCollection: active: true mutableTypes: - 'kotlin.collections.MutableList' - 'kotlin.collections.MutableMap' - 'kotlin.collections.MutableSet' - 'java.util.ArrayList' - 'java.util.LinkedHashSet' - 'java.util.HashSet' - 'java.util.LinkedHashMap' - 'java.util.HashMap' ElseCaseInsteadOfExhaustiveWhen: active: false ignoredSubjectTypes: [] EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: active: true ExitOutsideMain: active: false ExplicitGarbageCollectionCall: active: true HasPlatformType: active: true IgnoredReturnValue: active: true restrictToConfig: true returnValueAnnotations: - 'CheckResult' - '*.CheckResult' - 'CheckReturnValue' - '*.CheckReturnValue' ignoreReturnValueAnnotations: - 'CanIgnoreReturnValue' - '*.CanIgnoreReturnValue' returnValueTypes: - 'kotlin.sequences.Sequence' - 'kotlinx.coroutines.flow.*Flow' - 'java.util.stream.*Stream' ignoreFunctionCall: [] ImplicitDefaultLocale: active: true ImplicitUnitReturnType: active: false allowExplicitReturnType: true InvalidRange: active: true IteratorHasNextCallsNextMethod: active: true IteratorNotThrowingNoSuchElementException: active: true LateinitUsage: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true MissingPackageDeclaration: active: false excludes: ['**/*.kts'] NullCheckOnMutableProperty: active: false NullableToStringCall: active: false PropertyUsedBeforeDeclaration: active: false UnconditionalJumpStatementInLoop: active: false UnnecessaryNotNullCheck: active: false UnnecessaryNotNullOperator: active: true UnnecessarySafeCall: active: true UnreachableCatchBlock: active: true UnreachableCode: active: true UnsafeCallOnNullableType: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] UnsafeCast: active: true UnusedUnaryOperator: active: true UselessPostfixExpression: active: true WrongEqualsTypeParameter: active: true style: active: true AlsoCouldBeApply: active: false BracesOnIfStatements: active: false singleLine: 'never' multiLine: 'always' BracesOnWhenStatements: active: false singleLine: 'necessary' multiLine: 'consistent' CanBeNonNullable: active: false CascadingCallWrapping: active: false includeElvis: true ClassOrdering: active: false CollapsibleIfStatements: active: false DataClassContainsFunctions: active: false conversionFunctionPrefix: - 'to' allowOperators: false DataClassShouldBeImmutable: active: false DestructuringDeclarationWithTooManyEntries: active: true maxDestructuringEntries: 3 DoubleNegativeLambda: active: false negativeFunctions: - reason: 'Use `takeIf` instead.' value: 'takeUnless' - reason: 'Use `all` instead.' value: 'none' negativeFunctionNameParts: - 'not' - 'non' EqualsNullCall: active: true EqualsOnSignatureLine: active: false ExplicitCollectionElementAccessMethod: active: false ExplicitItLambdaParameter: active: true ExpressionBodySyntax: active: false includeLineWrapping: false ForbiddenAnnotation: active: false annotations: - reason: 'it is a java annotation. Use `Suppress` instead.' value: 'java.lang.SuppressWarnings' - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' value: 'java.lang.Deprecated' - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' value: 'java.lang.annotation.Documented' - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' value: 'java.lang.annotation.Target' - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' value: 'java.lang.annotation.Retention' - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' value: 'java.lang.annotation.Repeatable' - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' value: 'java.lang.annotation.Inherited' ForbiddenComment: active: false comments: - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' value: 'FIXME:' - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' value: 'STOPSHIP:' - reason: 'Forbidden TODO todo marker in comment, please do the changes.' value: 'TODO:' allowedPatterns: '' ForbiddenImport: active: false imports: [] forbiddenPatterns: '' ForbiddenMethodCall: active: false methods: - reason: 'print does not allow you to configure the output stream. Use a logger instead.' value: 'kotlin.io.print' - reason: 'println does not allow you to configure the output stream. Use a logger instead.' value: 'kotlin.io.println' ForbiddenSuppress: active: false rules: [] ForbiddenVoid: active: true ignoreOverridden: false ignoreUsageInGenerics: false FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true ignoreActualFunction: true excludedFunctions: [] LoopWithTooManyJumpStatements: active: true maxJumpCount: 1 MagicNumber: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] ignoreNumbers: - '-1' - '0' - '1' - '2' ignoreHashCodeFunction: true ignorePropertyDeclaration: true ignoreLocalVariableDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false ignoreRanges: true ignoreExtensionFunctions: true MandatoryBracesLoops: active: false MaxChainedCallsOnSameLine: active: false maxChainedCalls: 5 MaxLineLength: active: true maxLineLength: 120 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: false excludeRawStrings: true MayBeConst: active: true ModifierOrder: active: true MultilineLambdaItParameter: active: false MultilineRawStringIndentation: active: false indentSize: 4 trimmingMethods: - 'trimIndent' - 'trimMargin' NestedClassesVisibility: active: true NewLineAtEndOfFile: active: true NoTabs: active: false NullableBooleanCheck: active: false ObjectLiteralToLambda: active: true OptionalAbstractKeyword: active: true OptionalUnit: active: false PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: active: true RedundantExplicitType: active: false RedundantHigherOrderMapUsage: active: true RedundantVisibilityModifierRule: active: false ReturnCount: active: true max: 5 excludedFunctions: - 'equals' excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false SafeCast: active: true SerialVersionUIDInSerializableClass: active: true SpacingBetweenPackageAndImports: active: false StringShouldBeRawString: active: false maxEscapedCharacterCount: 2 ignoredCharacters: [] ThrowsCount: active: true max: 2 excludeGuardClauses: false TrailingWhitespace: active: false TrimMultilineRawString: active: false trimmingMethods: - 'trimIndent' - 'trimMargin' UnderscoresInNumericLiterals: active: false acceptableLength: 4 allowNonStandardGrouping: false UnnecessaryAbstractClass: active: true UnnecessaryAnnotationUseSiteTarget: active: false UnnecessaryApply: active: true UnnecessaryBackticks: active: false UnnecessaryBracesAroundTrailingLambda: active: false UnnecessaryFilter: active: true UnnecessaryInheritance: active: true UnnecessaryInnerClass: active: false UnnecessaryLet: active: false UnnecessaryParentheses: active: false allowForUnclearPrecedence: false UntilInsteadOfRangeTo: active: false UnusedImports: active: false UnusedParameter: active: true allowedNames: 'ignored|expected' UnusedPrivateClass: active: true UnusedPrivateMember: active: true allowedNames: '' ignoreAnnotated: ['Preview', 'PreviewPhone', 'PreviewTablet', 'PreviewFoldable'] UnusedPrivateProperty: active: true allowedNames: '_|ignored|expected|serialVersionUID' UseAnyOrNoneInsteadOfFind: active: true UseArrayLiteralsInAnnotations: active: true UseCheckNotNull: active: true UseCheckOrError: active: true UseDataClass: active: false allowVars: false UseEmptyCounterpart: active: false UseIfEmptyOrIfBlank: active: false UseIfInsteadOfWhen: active: false ignoreWhenContainingVariableDeclaration: false UseIsNullOrEmpty: active: true UseLet: active: false UseOrEmpty: active: true UseRequire: active: true UseRequireNotNull: active: true UseSumOfInsteadOfFlatMapSize: active: false UselessCallOnNotNull: active: true UtilityClassWithPublicConstructor: active: true VarCouldBeVal: active: true ignoreLateinitVar: false WildcardImport: active: true excludeImports: - 'java.util.*' formatting: active: true android: false autoCorrect: true AnnotationOnSeparateLine: active: true autoCorrect: true indentSize: 4 AnnotationSpacing: active: true autoCorrect: true ArgumentListWrapping: active: true autoCorrect: true indentSize: 4 maxLineLength: 120 BlockCommentInitialStarAlignment: active: true autoCorrect: true ChainWrapping: active: true autoCorrect: true indentSize: 4 ClassName: active: false CommentSpacing: active: true autoCorrect: true CommentWrapping: active: true autoCorrect: true indentSize: 4 ContextReceiverMapping: active: false autoCorrect: true maxLineLength: 120 indentSize: 4 DiscouragedCommentLocation: active: false autoCorrect: true EnumEntryNameCase: active: true autoCorrect: true EnumWrapping: active: false autoCorrect: true indentSize: 4 Filename: active: true FinalNewline: active: true autoCorrect: true insertFinalNewLine: true FunKeywordSpacing: active: true autoCorrect: true FunctionName: active: false FunctionReturnTypeSpacing: active: true autoCorrect: true maxLineLength: 120 FunctionSignature: active: false autoCorrect: true forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647 functionBodyExpressionWrapping: 'default' maxLineLength: 120 indentSize: 4 FunctionStartOfBodySpacing: active: true autoCorrect: true FunctionTypeReferenceSpacing: active: true autoCorrect: true IfElseBracing: active: false autoCorrect: true indentSize: 4 IfElseWrapping: active: false autoCorrect: true indentSize: 4 ImportOrdering: active: true autoCorrect: true layout: '*,java.**,javax.**,kotlin.**,^' Indentation: active: true autoCorrect: true indentSize: 4 KdocWrapping: active: true autoCorrect: true indentSize: 4 MaximumLineLength: active: false maxLineLength: 120 ignoreBackTickedIdentifier: false ModifierListSpacing: active: true autoCorrect: true ModifierOrdering: active: true autoCorrect: true MultiLineIfElse: active: true autoCorrect: true indentSize: 4 MultilineExpressionWrapping: active: false autoCorrect: true indentSize: 4 NoBlankLineBeforeRbrace: active: true autoCorrect: true NoBlankLineInList: active: false autoCorrect: true NoBlankLinesInChainedMethodCalls: active: true autoCorrect: true NoConsecutiveBlankLines: active: true autoCorrect: true NoConsecutiveComments: active: false NoEmptyClassBody: active: true autoCorrect: true NoEmptyFirstLineInClassBody: active: false autoCorrect: true indentSize: 4 NoEmptyFirstLineInMethodBlock: active: true autoCorrect: true NoLineBreakAfterElse: active: true autoCorrect: true NoLineBreakBeforeAssignment: active: true autoCorrect: true NoMultipleSpaces: active: true autoCorrect: true NoSemicolons: active: true autoCorrect: true NoSingleLineBlockComment: active: false autoCorrect: true indentSize: 4 NoTrailingSpaces: active: true autoCorrect: true NoUnitReturn: active: true autoCorrect: true NoUnusedImports: active: true autoCorrect: true NoWildcardImports: active: true packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**' NullableTypeSpacing: active: true autoCorrect: true PackageName: active: true autoCorrect: true ParameterListSpacing: active: false autoCorrect: true ParameterListWrapping: active: true autoCorrect: true maxLineLength: 120 indentSize: 4 ParameterWrapping: active: true autoCorrect: true indentSize: 4 maxLineLength: 120 PropertyName: active: false PropertyWrapping: active: true autoCorrect: true indentSize: 4 maxLineLength: 120 SpacingAroundAngleBrackets: active: true autoCorrect: true SpacingAroundColon: active: true autoCorrect: true SpacingAroundComma: active: true autoCorrect: true SpacingAroundCurly: active: true autoCorrect: true SpacingAroundDot: active: true autoCorrect: true SpacingAroundDoubleColon: active: true autoCorrect: true SpacingAroundKeyword: active: true autoCorrect: true SpacingAroundOperators: active: true autoCorrect: true SpacingAroundParens: active: true autoCorrect: true SpacingAroundRangeOperator: active: true autoCorrect: true SpacingAroundUnaryOperator: active: true autoCorrect: true SpacingBetweenDeclarationsWithAnnotations: active: true autoCorrect: true SpacingBetweenDeclarationsWithComments: active: true autoCorrect: true SpacingBetweenFunctionNameAndOpeningParenthesis: active: true autoCorrect: true StringTemplate: active: true autoCorrect: true StringTemplateIndent: active: false autoCorrect: true indentSize: 4 TrailingCommaOnCallSite: active: true autoCorrect: true useTrailingCommaOnCallSite: true TrailingCommaOnDeclarationSite: active: true autoCorrect: true useTrailingCommaOnDeclarationSite: true TryCatchFinallySpacing: active: false autoCorrect: true indentSize: 4 TypeArgumentListSpacing: active: false autoCorrect: true indentSize: 4 TypeParameterListSpacing: active: false autoCorrect: true indentSize: 4 UnnecessaryParenthesesBeforeTrailingLambda: active: true autoCorrect: true Wrapping: active: true autoCorrect: true indentSize: 4 maxLineLength: 120 Compose: ComposableAnnotationNaming: active: true ComposableNaming: active: true # -- You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters) # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter ComposableParamOrder: active: true # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) # treatAsLambda: MyLambdaType CompositionLocalAllowlist: active: true # -- You can optionally define a list of CompositionLocals that are allowed here allowedCompositionLocals: LocalComponentContext, LocalFeedFlowStrings CompositionLocalNaming: active: true ContentEmitterReturningValues: active: true # -- You can optionally add your own composables here # contentEmitters: MyComposable,MyOtherComposable DefaultsVisibility: active: true LambdaParameterInRestartableEffect: active: true # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) # treatAsLambda: MyLambdaType ModifierClickableOrder: active: true # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierComposable: active: true # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierMissing: active: true # -- You can optionally control the visibility of which composables to check for here # -- Possible values are: `only_public`, `public_and_internal` and `all` (default is `only_public`) # checkModifiersForVisibility: only_public # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierNaming: active: true # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierNotUsedAtRoot: active: true # -- You can optionally add your own composables here # contentEmitters: MyComposable,MyOtherComposable # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierReused: active: true # -- You can optionally add your own Modifier types # customModifiers: BananaModifier,PotatoModifier ModifierWithoutDefault: active: true MultipleEmitters: active: true # -- You can optionally add your own composables here that will count as content emitters # contentEmitters: MyComposable,MyOtherComposable # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals) # contentEmittersDenylist: MyNonEmitterComposable MutableParams: active: true MutableStateParam: active: true PreviewAnnotationNaming: active: true PreviewPublic: active: true RememberMissing: active: true RememberContentMissing: active: true UnstableCollections: active: true ViewModelForwarding: active: true # -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here: # allowedStateHolderNames: .*ViewModel,.*Presenter # -- You can optionally add an allowlist for Composable names that won't be affected by this rule # allowedForwarding: .*Content,.*FancyStuff ViewModelInjection: active: true # -- You can optionally add your own ViewModel factories here # viewModelFactories: hiltViewModel,potatoViewModelk ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] activity-compose = "1.12.0" android-compileSdk = "36" android-minSdk = "26" android-targetSdk = "36" androidx-test = "1.7.0" androidx-test-runner = "1.7.0" androidx-test-ext = "1.3.0" biometric = "1.2.0-alpha05" compose = "1.10.0-beta02" compose-androidx = "1.9.5" immutable-collections = "0.4.0" coroutines = "1.10.2" agp = "8.10.1" detekt = "1.23.8" detekt-compose-rules = "0.4.28" java = "21" kermit = "2.0.8" koin = "4.1.1" kotlin = "2.2.20" kotlinx-date-time = "0.7.1" composeMaterialIconsExtended = "1.7.3" multiplatform-settings = "1.3.0" lifecycle-navigation3 = "2.10.0-alpha05" navigation3 = "1.0.0-alpha05" kotlinx-serialization = "1.8.0" lifecycle = "2.10.0" sqlDelight = "2.2.1" turbine = "1.2.1" triplet-play = "3.12.2" roborazzi = "1.52.0" robolectric = "4.12.2" compose-material3 = "1.9.0-beta03" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometric" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } cashapp-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose" } compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-androidx" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-androidx" } compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" } jetbrains-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } androidx-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle-navigation3" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlinx-coroutine-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutine-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-date-time" } compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "compose" } compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-material3" } compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMaterialIconsExtended" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" } russhwolf-multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } russhwolf-multiplatform-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" } sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } sqldelight-coroutine-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" } touchlab-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version.ref = "detekt-compose-rules" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazziJunitRule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose" } immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutable-collections" } [bundles] androidx-test = ["androidx-test-core", "androidx-test-ext-junit", "androidx-test-rules", "androidx-test-runner"] [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } kmp-detekt = { id = "com.moneyflow.detekt", version = "unspecified" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } triplet-play = { id = "com.github.triplet.play", version.ref = "triplet-play" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ appVersionCode=1 appVersionName=1.0 releaseBuild=false kotlin.code.style=official org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" android.useAndroidX=true android.enableJetifier=false kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true kotlin.mpp.androidSourceSetLayoutVersion=2 xcodeproj=iosApp/iosApp.xcworkspace # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true # Speed up the build org.gradle.caching=true org.gradle.parallel=true android.nonFinalResIds=false // TODO: Add configuration cache ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: iosApp/.scripts/version.sh ================================================ #!/bin/sh -euo pipefail VERSION_PROPERTIES_FILE="${SRCROOT}/../version.properties" if [ ! -f "$VERSION_PROPERTIES_FILE" ]; then echo "Error: version.properties not found at $VERSION_PROPERTIES_FILE. SRCROOT is $SRCROOT" exit 1 fi MAJOR=$(grep '^MAJOR=' "$VERSION_PROPERTIES_FILE" | cut -d'=' -f2) MINOR=$(grep '^MINOR=' "$VERSION_PROPERTIES_FILE" | cut -d'=' -f2) PATCH=$(grep '^PATCH=' "$VERSION_PROPERTIES_FILE" | cut -d'=' -f2) if [ -z "$MAJOR" ] || [ -z "$MINOR" ] || [ -z "$PATCH" ]; then echo "Error: Could not read MAJOR, MINOR, or PATCH from $VERSION_PROPERTIES_FILE" exit 1 fi VERSION_NAME="${MAJOR}.${MINOR}.${PATCH}" if [ -n "${GITHUB_RUN_NUMBER:-}" ]; then VERSION_CODE=$((GITHUB_RUN_NUMBER + 1000)) else VERSION_CODE="2" fi mkdir -p "${SRCROOT}/Plist" cat < "${SRCROOT}/Plist/Prefix" #define VERSION_NAME ${VERSION_NAME} #define VERSION_CODE ${VERSION_CODE} EOF ================================================ FILE: iosApp/Assets/DebugIcon.icon/icon.json ================================================ { "fill" : { "solid" : "srgb:1.00000,0.75686,0.02745,1.00000" }, "groups" : [ { "layers" : [ { "glass" : true, "image-name" : "savings_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24 2.svg", "name" : "savings_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24 2", "position" : { "scale" : 40, "translation-in-points" : [ 4, -3 ] } } ], "name" : "Group", "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } ================================================ FILE: iosApp/Assets/Icon.icon/icon.json ================================================ { "fill" : { "solid" : "srgb:0.00000,0.58824,0.53333,1.00000" }, "groups" : [ { "layers" : [ { "glass" : true, "image-name" : "savings_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24 2.svg", "name" : "savings_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24 2", "position" : { "scale" : 40, "translation-in-points" : [ 4, -3 ] } } ], "name" : "Group", "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } ================================================ FILE: iosApp/Assets/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleDisplayName MoneyFlow CFBundleShortVersionString VERSION_NAME CFBundleVersion VERSION_CODE LSRequiresIPhoneOS UILaunchScreen UIApplicationSupportsIndirectInputEvents UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight CADisableMinimumFrameDurationOnPhone NSFaceIDUsageDescription Unlock your MoneyFlow data with Face ID. ITSAppUsesNonExemptEncryption ================================================ FILE: iosApp/Configuration/Config.xcconfig ================================================ TEAM_ID= PRODUCT_NAME=MoneyFlow PRODUCT_BUNDLE_IDENTIFIER=com.prof18.moneyflow.MoneyFlow$(TEAM_ID) CURRENT_PROJECT_VERSION=1 MARKETING_VERSION=1.0 ================================================ FILE: iosApp/MoneyFlow.entitlements ================================================ com.apple.security.application-groups group.com.prof18.moneyflow ================================================ FILE: iosApp/MoneyFlow.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXAggregateTarget section */ 13F46F372EDA1FAC003E22ED /* VersionGenerator */ = { isa = PBXAggregateTarget; buildConfigurationList = 13F46F3A2EDA1FAC003E22ED /* Build configuration list for PBXAggregateTarget "VersionGenerator" */; buildPhases = ( 13F46F3B2EDA1FBE003E22ED /* Generate Version */, ); dependencies = ( ); name = VersionGenerator; packageProductDependencies = ( ); productName = VersionGenerator; }; /* End PBXAggregateTarget section */ /* Begin PBXContainerItemProxy section */ 13F46F3C2EDA1FF5003E22ED /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1BCCA59B08481063B39D0DC7 /* Project object */; proxyType = 1; remoteGlobalIDString = 13F46F372EDA1FAC003E22ED; remoteInfo = VersionGenerator; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 13F46F0F2ED9A30F003E22ED /* MoneyFlow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MoneyFlow.entitlements; sourceTree = ""; }; 74302460264A6AC12ABA3020 /* MoneyFlow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MoneyFlow.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 13F46DDF2ED99F4A003E22ED /* Exceptions for "Assets" folder in "MoneyFlow" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = 5E0725B4FAD85C89202B3C65 /* MoneyFlow */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 13F46DDE2ED99F2C003E22ED /* Assets */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 13F46DDF2ED99F4A003E22ED /* Exceptions for "Assets" folder in "MoneyFlow" target */, ); path = Assets; sourceTree = ""; }; 6E007A3526BC97399FEA46B2 /* Source */ = { isa = PBXFileSystemSynchronizedRootGroup; path = Source; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ 8EB6971E0154A666A3741BB2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ B7DE119FFFE2D8F48EB1B463 /* Products */ = { isa = PBXGroup; children = ( 74302460264A6AC12ABA3020 /* MoneyFlow.app */, ); name = Products; sourceTree = ""; }; D9916947448D05C97BC70C16 = { isa = PBXGroup; children = ( 13F46F0F2ED9A30F003E22ED /* MoneyFlow.entitlements */, 13F46DDE2ED99F2C003E22ED /* Assets */, 6E007A3526BC97399FEA46B2 /* Source */, B7DE119FFFE2D8F48EB1B463 /* Products */, ); sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 5E0725B4FAD85C89202B3C65 /* MoneyFlow */ = { isa = PBXNativeTarget; buildConfigurationList = 0506720A08F22077D1ED93AA /* Build configuration list for PBXNativeTarget "MoneyFlow" */; buildPhases = ( B1ED7E9E2F2C4795BA9C3825 /* Generate Version */, 18C25BC25EBD5C0382F00434 /* Compile Kotlin Framework */, 2D7CA7880B8FD75751B1FE34 /* Sources */, 8EB6971E0154A666A3741BB2 /* Frameworks */, 9A86C77568EE9331093603CE /* Resources */, ); buildRules = ( ); dependencies = ( 13F46F3D2EDA1FF5003E22ED /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 13F46DDE2ED99F2C003E22ED /* Assets */, 6E007A3526BC97399FEA46B2 /* Source */, ); name = MoneyFlow; packageProductDependencies = ( ); productName = MoneyFlow; productReference = 74302460264A6AC12ABA3020 /* MoneyFlow.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 1BCCA59B08481063B39D0DC7 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1620; TargetAttributes = { 13F46F372EDA1FAC003E22ED = { CreatedOnToolsVersion = 26.1.1; }; 5E0725B4FAD85C89202B3C65 = { CreatedOnToolsVersion = 16.2; }; }; }; buildConfigurationList = E8BA6EDC49E528ED52578D5F /* Build configuration list for PBXProject "MoneyFlow" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = D9916947448D05C97BC70C16; minimizedProjectReferenceProxies = 1; preferredProjectObjectVersion = 77; productRefGroup = B7DE119FFFE2D8F48EB1B463 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 5E0725B4FAD85C89202B3C65 /* MoneyFlow */, 13F46F372EDA1FAC003E22ED /* VersionGenerator */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 9A86C77568EE9331093603CE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 13F46F3B2EDA1FBE003E22ED /* Generate Version */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Generate Version"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$SRCROOT\"/.scripts/version.sh\n"; }; 18C25BC25EBD5C0382F00434 /* Compile Kotlin Framework */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Compile Kotlin Framework"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; }; B1ED7E9E2F2C4795BA9C3825 /* Generate Version */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Generate Version"; outputFileListPaths = ( ); outputPaths = ( "$(SRCROOT)/Plist/Prefix", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$SRCROOT\"/.scripts/version.sh\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 2D7CA7880B8FD75751B1FE34 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 13F46F3D2EDA1FF5003E22ED /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 13F46F372EDA1FAC003E22ED /* VersionGenerator */; targetProxy = 13F46F3C2EDA1FF5003E22ED /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 13F46F382EDA1FAC003E22ED /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Q7CUB3RNAK; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 13F46F392EDA1FAC003E22ED /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Q7CUB3RNAK; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; 49119FB90DF9E7FB34FEBE73 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = DebugIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = MoneyFlow.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = Q7CUB3RNAK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Assets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MoneyFlow; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_PREFIX_HEADER = Plist/Prefix; INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; OTHER_LDFLAGS = ( "-lsqlite3", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = com.prof18.moneyflow.dev; PRODUCT_NAME = MoneyFlow; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 58BB095DEEA5C79A2751010D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 72DCDEE7F3F29D583F48F4FE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = Icon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = MoneyFlow.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Q7CUB3RNAK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Assets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MoneyFlow; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_PREFIX_HEADER = Plist/Prefix; INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; OTHER_LDFLAGS = ( "-lsqlite3", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = com.prof18.moneyflow; PRODUCT_NAME = MoneyFlow; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MoneyFlowGHActionDistributionProvisioning; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; E3B514BB52F07F94A4AE1E8C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 0506720A08F22077D1ED93AA /* Build configuration list for PBXNativeTarget "MoneyFlow" */ = { isa = XCConfigurationList; buildConfigurations = ( 49119FB90DF9E7FB34FEBE73 /* Debug */, 72DCDEE7F3F29D583F48F4FE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 13F46F3A2EDA1FAC003E22ED /* Build configuration list for PBXAggregateTarget "VersionGenerator" */ = { isa = XCConfigurationList; buildConfigurations = ( 13F46F382EDA1FAC003E22ED /* Debug */, 13F46F392EDA1FAC003E22ED /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; E8BA6EDC49E528ED52578D5F /* Build configuration list for PBXProject "MoneyFlow" */ = { isa = XCConfigurationList; buildConfigurations = ( 58BB095DEEA5C79A2751010D /* Debug */, E3B514BB52F07F94A4AE1E8C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 1BCCA59B08481063B39D0DC7 /* Project object */; } ================================================ FILE: iosApp/MoneyFlow.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: iosApp/MoneyFlow.xcodeproj/xcshareddata/xcschemes/MoneyFlow.xcscheme ================================================ ================================================ FILE: iosApp/Source/ContentView.swift ================================================ import UIKit import SwiftUI import MoneyFlowKit struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } struct ContentView: View { var body: some View { ComposeView() .ignoresSafeArea() } } ================================================ FILE: iosApp/Source/DI/Koin.swift ================================================ // // DIContainer.swift // App () // // Created by Marco Gomiero on 05/11/2020. // import Foundation import MoneyFlowKit func startKoin() { _ = KoinIosKt.doInitKoinIos() } ================================================ FILE: iosApp/Source/MoneyFlowApp.swift ================================================ import SwiftUI @main struct MoneyFlowApp: App { init() { startKoin() } var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ], "packageRules": [ { "matchPackagePatterns": [ "androidx.compose.compiler:compiler" ], "groupName": "kotlin" }, { "matchPackagePatterns": [ "org.jetbrains.kotlin.*" ], "groupName": "kotlin" }, { "matchPackagePatterns": [ "com.google.devtools.ksp" ], "groupName": "kotlin" } ] } ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { includeBuild("build-logic") repositories { google() gradlePluginPortal() mavenCentral() } } dependencyResolutionManagement { repositories { google() mavenCentral() mavenLocal() } } plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "money-flow" include(":shared") include(":androidApp") ================================================ FILE: setup.sh ================================================ #!/bin/bash set -ex export ANDROID_SDK_ROOT="/root/android-sdk" apt-get update && apt-get install -y expect mkdir -p "$ANDROID_SDK_ROOT/licenses" echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_SDK_ROOT/licenses/android-sdk-license" mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" cd "$ANDROID_SDK_ROOT/cmdline-tools" curl -sSL -o tools.zip https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip unzip -q tools.zip rm tools.zip mkdir -p latest mv cmdline-tools/* latest/ export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null expect < iosTarget.binaries.framework { baseName = "MoneyFlowKit" isStatic = true linkerOpts += listOf("-lsqlite3") } } sourceSets { applyDefaultHierarchyTemplate() sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") languageSettings.optIn("com.russhwolf.settings.ExperimentalSettingsImplementation") languageSettings.optIn("kotlin.experimental.ExperimentalObjCRefinement") languageSettings.optIn("kotlin.experimental.ExperimentalObjCName") languageSettings.optIn("kotlin.time.ExperimentalTime") } val commonMain by getting { dependencies { api(libs.compose.runtime) api(libs.compose.foundation) api(libs.compose.foundation) api(libs.compose.material3) implementation(libs.compose.material.icons.extended) api(libs.compose.ui) implementation(libs.compose.components.resources) implementation(libs.jetbrains.ui.tooling.preview) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.koin.compose.viewmodel.navigation) implementation(libs.koin.compose) implementation(libs.kotlinx.serialization.core) implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.kotlinx.coroutine.core) implementation(libs.koin.core) implementation(libs.kotlinx.datetime) implementation(libs.russhwolf.multiplatform.settings) implementation(libs.immutable.collections) api(libs.touchlab.kermit) implementation(libs.sqldelight.runtime) implementation(libs.sqldelight.coroutine.extensions) } } val commonTest by getting { dependencies { implementation(kotlin("test")) implementation(libs.koin.test) implementation(libs.kotlinx.coroutine.test) implementation(libs.cashapp.turbine) implementation(libs.russhwolf.multiplatform.settings.test) } } val androidMain by getting { dependencies { api(libs.androidx.activity.compose) api(libs.androidx.lifecycle.viewmodel.ktx) api(libs.koin.android) api(libs.androidx.biometric.ktx) implementation(libs.sqldelight.android.driver) } } val androidUnitTest by getting { dependencies { implementation(kotlin("test")) implementation(libs.kotlin.test.junit) implementation(libs.bundles.androidx.test) implementation(libs.kotlinx.coroutine.test) implementation(libs.sqldelight.sqlite.driver) implementation(libs.roborazzi) implementation(libs.roborazziJunitRule) implementation(libs.roborazzi.compose) implementation(libs.compose.ui.test) implementation(libs.robolectric) } } val iosMain by getting { dependencies { implementation(libs.kotlinx.coroutine.core) implementation(libs.sqldelight.native.driver) } } } } android { namespace = "com.prof18.moneyflow" compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } testOptions { unitTests.isIncludeAndroidResources = true } compileOptions { sourceCompatibility = javaVersion targetCompatibility = javaVersion } buildFeatures { compose = true } } dependencies { debugImplementation(libs.jetbrains.ui.tooling.preview) androidTestImplementation(libs.compose.ui.test) } tasks.withType().configureEach { systemProperty("roborazzi.test.record.dir", rootProject.layout.projectDirectory.dir("image/roborazzi").asFile.path) listOf( "http" to System.getenv("http_proxy"), "https" to System.getenv("https_proxy"), ).forEach { (scheme, proxyValue) -> proxyValue ?.takeIf { it.isNotBlank() } ?.let(::URI) ?.let { proxyUri -> proxyUri.host?.let { host -> systemProperty("$scheme.proxyHost", host) } proxyUri.port.takeIf { it != -1 }?.let { port -> systemProperty("$scheme.proxyPort", port) } } } } sqldelight { databases { create("MoneyFlowDB") { packageName.set("com.prof18.moneyflow.db") schemaOutputDirectory.set(file("src/commonMain/sqldelight/com/prof18/moneyflow/schema")) } } } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/AndroidBiometricAuthenticator.kt ================================================ package com.prof18.moneyflow import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import com.prof18.moneyflow.features.authentication.BiometricAuthenticator public class AndroidBiometricAuthenticator( private val activity: FragmentActivity, ) : BiometricAuthenticator { private var onSuccess: (() -> Unit)? = null private var onFailure: (() -> Unit)? = null private var onError: (() -> Unit)? = null private val biometricPrompt: BiometricPrompt by lazy { val executor = ContextCompat.getMainExecutor(activity) BiometricPrompt( activity, executor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError( errorCode: Int, errString: CharSequence, ) { super.onAuthenticationError(errorCode, errString) onError?.invoke() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) onSuccess?.invoke() } override fun onAuthenticationFailed() { super.onAuthenticationFailed() onFailure?.invoke() } }, ) } private val promptInfo: BiometricPrompt.PromptInfo by lazy { BiometricPrompt.PromptInfo.Builder() .setTitle("MoneyFlow") .setSubtitle("Unlock MoneyFlow") .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) .build() } override fun canAuthenticate(): Boolean { val biometricManager = BiometricManager.from(activity) return biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS } override fun authenticate( onSuccess: () -> Unit, onFailure: () -> Unit, onError: () -> Unit, ) { this.onSuccess = onSuccess this.onFailure = onFailure this.onError = onError biometricPrompt.authenticate(promptInfo) } } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/AndroidBiometricAvailabilityChecker.kt ================================================ package com.prof18.moneyflow import android.content.Context import androidx.biometric.BiometricManager import co.touchlab.kermit.Logger import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker internal class AndroidBiometricAvailabilityChecker( private val context: Context, ) : BiometricAvailabilityChecker { override fun isBiometricSupported(): Boolean { val biometricManager = BiometricManager.from(context) val authResult = biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL, ) return when (authResult) { BiometricManager.BIOMETRIC_SUCCESS -> true else -> { Logger.d { "Biometric not supported or not available on this device." } false } } } } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/database/DatabaseDriverFactory.kt ================================================ package com.prof18.moneyflow.database import android.content.Context import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import com.prof18.moneyflow.db.MoneyFlowDB internal fun createDatabaseDriver(context: Context, useDebugDatabaseName: Boolean = false): SqlDriver { return AndroidSqliteDriver( schema = MoneyFlowDB.Schema, context = context, name = if (useDebugDatabaseName) { DatabaseHelper.APP_DATABASE_NAME_DEBUG } else { DatabaseHelper.APP_DATABASE_NAME_PROD }, ) } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/di/KoinAndroid.kt ================================================ package com.prof18.moneyflow.di import com.prof18.moneyflow.AndroidBiometricAvailabilityChecker import com.prof18.moneyflow.database.DatabaseHelper import com.prof18.moneyflow.database.createDatabaseDriver import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings import kotlinx.coroutines.Dispatchers import org.koin.core.module.Module import org.koin.dsl.module internal actual val platformModule: Module = module { single { createDatabaseDriver(get()) } single { DatabaseHelper(get(), Dispatchers.Default) } single { val factory: Settings.Factory = SharedPreferencesSettings.Factory(get()) factory.create() } single { AndroidBiometricAvailabilityChecker(get()) } } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/utils/LocalAppLocale.android.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalResources import java.util.Locale internal actual object LocalAppLocale { private var default: Locale? = null actual val current: String @Composable get() = Locale.getDefault().toString() @Composable actual infix fun provides(value: String?): ProvidedValue<*> { val configuration = LocalConfiguration.current if (default == null) { default = Locale.getDefault() } val new = when (value) { null -> default!! else -> Locale(value) } Locale.setDefault(new) configuration.setLocale(new) val resources = LocalResources.current resources.updateConfiguration(configuration, resources.displayMetrics) return LocalConfiguration.provides(configuration) } } ================================================ FILE: shared/src/androidMain/kotlin/com/prof18/moneyflow/utils/LocalAppTheme.android.kt ================================================ package com.prof18.moneyflow.utils import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.platform.LocalConfiguration internal actual object LocalAppTheme { actual val current: Boolean @Composable get() = (LocalConfiguration.current.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES @Composable actual infix fun provides(value: Boolean?): ProvidedValue<*> { val new = if (value == null) { LocalConfiguration.current } else { Configuration(LocalConfiguration.current).apply { uiMode = when (value) { true -> (uiMode and UI_MODE_NIGHT_MASK.inv()) or UI_MODE_NIGHT_YES false -> (uiMode and UI_MODE_NIGHT_MASK.inv()) or UI_MODE_NIGHT_NO } } } return LocalConfiguration.provides(new) } } ================================================ FILE: shared/src/androidMain/res/values/themes.xml ================================================ ================================================ FILE: shared/src/androidMain/res/values-night/themes.xml ================================================ ================================================ FILE: shared/src/androidUnitTest/AndroidManifest.xml ================================================ ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AddTransactionRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.presentation.addtransaction.AddTransactionScreen import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import kotlin.time.Clock @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class AddTransactionRoborazziTest : RoborazziTestBase() { @Test fun captureAddTransactionScreen() { composeRule.setContent { MoneyFlowTheme { AddTransactionScreen( categoryState = remember { mutableStateOf(RoborazziSampleData.sampleCategory) }, navigateUp = {}, navigateToCategoryList = {}, addTransaction = {}, amountText = "10.00", updateAmountText = {}, descriptionText = "Pizza 🍕", updateDescriptionText = {}, selectedTransactionType = TransactionType.OUTCOME, updateTransactionType = {}, updateSelectedDate = {}, dateLabel = "11 July 2021", selectedDateMillis = Clock.System.now().toEpochMilliseconds(), addTransactionAction = null, resetAction = {}, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, ) } } capture("add_transaction_screen") } @Test fun captureAddTransactionDatePicker() { val dateLabel = "11 July 2021" composeRule.setContent { MoneyFlowTheme { AddTransactionScreen( categoryState = remember { mutableStateOf(RoborazziSampleData.sampleCategory) }, navigateUp = {}, navigateToCategoryList = {}, addTransaction = {}, amountText = "10.00", updateAmountText = {}, descriptionText = "Pizza 🍕", updateDescriptionText = {}, selectedTransactionType = TransactionType.OUTCOME, updateTransactionType = {}, updateSelectedDate = {}, dateLabel = dateLabel, selectedDateMillis = Clock.System.now().toEpochMilliseconds(), addTransactionAction = null, resetAction = {}, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, ) } } composeRule.onNodeWithText(dateLabel).performClick() capture("add_transaction_screen_date_picker") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AllTransactionsRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.features.alltransactions.AllTransactionsUiState import com.prof18.moneyflow.presentation.alltransactions.AllTransactionsScreen import com.prof18.moneyflow.ui.style.MoneyFlowTheme import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class AllTransactionsRoborazziTest : RoborazziTestBase() { @Test fun captureAllTransactionsScreen() { composeRule.setContent { MoneyFlowTheme { AllTransactionsScreen( stateFlow = MutableStateFlow( AllTransactionsUiState( transactions = RoborazziSampleData.sampleTransactions, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, ), ), loadNextPage = {}, ) } } capture("all_transactions_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AuthRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.presentation.auth.AuthScreen import com.prof18.moneyflow.presentation.auth.AuthState import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class AuthRoborazziTest : RoborazziTestBase() { @Test fun captureAuthScreen() { composeRule.setContent { MoneyFlowTheme { AuthScreen( authState = AuthState.AUTH_IN_PROGRESS, onRetryClick = {}, ) } } capture("auth_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/BudgetAndRecapRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.presentation.budget.BudgetScreen import com.prof18.moneyflow.presentation.recap.RecapScreen import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class BudgetAndRecapRoborazziTest : RoborazziTestBase() { @Test fun captureBudgetScreen() { composeRule.setContent { MoneyFlowTheme { BudgetScreen() } } capture("budget_screen") } @Test fun captureRecapScreen() { composeRule.setContent { MoneyFlowTheme { RecapScreen() } } capture("recap_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/CategoriesRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.presentation.categories.CategoriesScreen import com.prof18.moneyflow.presentation.categories.CategoryModel import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class CategoriesRoborazziTest : RoborazziTestBase() { @Test fun captureCategoriesScreen() { composeRule.setContent { MoneyFlowTheme { CategoriesScreen( navigateUp = {}, sendCategoryBack = {}, isFromAddTransaction = true, categoryModel = CategoryModel.CategoryState( categories = listOf( Category( id = 0, name = "Food", icon = CategoryIcon.IC_HAMBURGER_SOLID, type = TransactionType.OUTCOME, createdAtMillis = 0, ), Category( id = 1, name = "Drinks", icon = CategoryIcon.IC_COCKTAIL_SOLID, type = TransactionType.OUTCOME, createdAtMillis = 0, ), ), ), ) } } capture("categories_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/ComponentsRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.presentation.model.UIErrorMessage import com.prof18.moneyflow.ui.components.MFTopBar import com.prof18.moneyflow.ui.components.TransactionCard import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.error_get_categories_message import money_flow.shared.generated.resources.settings_screen import org.jetbrains.compose.resources.stringResource import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class ComponentsRoborazziTest : RoborazziTestBase() { @Test fun captureTopBar() { composeRule.setContent { MoneyFlowTheme { MFTopBar( topAppBarText = stringResource(Res.string.settings_screen), actionTitle = "Save", onBackPressed = {}, onActionClicked = {}, actionEnabled = false, ) } } capture("component_top_bar") } @Test fun captureTransactionCard() { composeRule.setContent { MoneyFlowTheme { TransactionCard( transaction = RoborazziSampleData.sampleTransactions.first(), onLongPress = {}, onClick = {}, hideSensitiveData = true, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, ) } } capture("component_transaction_card") } @Test fun captureErrorView() { composeRule.setContent { MoneyFlowTheme { com.prof18.moneyflow.ui.components.ErrorView( uiErrorMessage = UIErrorMessage( message = Res.string.error_get_categories_message, ), ) } } capture("component_error_view") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/HomeRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.compose.material3.Scaffold import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.domain.entities.BalanceRecap import com.prof18.moneyflow.presentation.home.HomeModel import com.prof18.moneyflow.presentation.home.HomeScreen import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class HomeRoborazziTest : RoborazziTestBase() { @Test fun captureHomeScreen() { composeRule.setContent { MoneyFlowTheme { Scaffold { HomeScreen( homeModel = HomeModel.HomeState( balanceRecap = BalanceRecap( totalBalanceCents = 500_000, monthlyIncomeCents = 100_000, monthlyExpensesCents = 50_00, ), latestTransactions = RoborazziSampleData.sampleTransactions, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, ), hideSensitiveDataState = false, navigateToAllTransactions = {}, paddingValues = it, ) } } } capture("home_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/MoneyFlowLockedRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.data.SettingsRepository import com.prof18.moneyflow.data.settings.SettingsSource import com.prof18.moneyflow.features.addtransaction.AddTransactionViewModel import com.prof18.moneyflow.features.alltransactions.AllTransactionsViewModel import com.prof18.moneyflow.features.authentication.BiometricAuthenticator import com.prof18.moneyflow.features.categories.CategoriesViewModel import com.prof18.moneyflow.features.home.HomeViewModel import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.features.settings.SettingsViewModel import com.prof18.moneyflow.presentation.MoneyFlowApp import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.utilities.closeDriver import com.prof18.moneyflow.utilities.createDriver import com.prof18.moneyflow.utilities.getDatabaseHelper import com.russhwolf.settings.MapSettings import com.russhwolf.settings.Settings import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.core.module.dsl.viewModel import org.koin.dsl.module import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class MoneyFlowLockedRoborazziTest : RoborazziTestBase() { private val fakeBiometricAuthenticator = object : BiometricAuthenticator { override fun canAuthenticate(): Boolean = true override fun authenticate( onSuccess: () -> Unit, onFailure: () -> Unit, onError: () -> Unit, ) { onFailure() } } @Before fun setup() { createDriver() stopKoin() // Ensure Koin is stopped before starting val koinApplication = startKoin { modules( module { single { getDatabaseHelper() } single { MapSettings() } single { SettingsSource(get()) } single { SettingsRepository(get()) } single { MoneyRepository(get()) } single { MoneyFlowErrorMapper() } single { object : BiometricAvailabilityChecker { override fun isBiometricSupported(): Boolean = true } } viewModel { HomeViewModel(get(), get(), get()) } viewModel { AddTransactionViewModel(get(), get()) } viewModel { CategoriesViewModel(get(), get()) } viewModel { AllTransactionsViewModel(get(), get()) } viewModel { SettingsViewModel(get()) } viewModel { MainViewModel(get(), get()) } }, ) } koinApplication.koin.get().setBiometric(true) } @After fun teardownResources() { stopKoin() closeDriver() } @Test fun captureMoneyFlowLockedUi() { composeRule.setContent { MoneyFlowTheme { MoneyFlowApp( biometricAuthenticator = fakeBiometricAuthenticator, ) } } capture("money_flow_locked") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/MoneyFlowNavHostRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.data.SettingsRepository import com.prof18.moneyflow.data.settings.SettingsSource import com.prof18.moneyflow.features.addtransaction.AddTransactionViewModel import com.prof18.moneyflow.features.alltransactions.AllTransactionsViewModel import com.prof18.moneyflow.features.categories.CategoriesViewModel import com.prof18.moneyflow.features.home.HomeViewModel import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.features.settings.SettingsViewModel import com.prof18.moneyflow.navigation.MoneyFlowNavHost import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.utilities.closeDriver import com.prof18.moneyflow.utilities.createDriver import com.prof18.moneyflow.utilities.getDatabaseHelper import com.russhwolf.settings.MapSettings import com.russhwolf.settings.Settings import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.core.module.dsl.viewModel import org.koin.dsl.module import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class MoneyFlowNavHostRoborazziTest : RoborazziTestBase() { @Before fun setup() { createDriver() stopKoin() // Ensure Koin is stopped before starting startKoin { modules( module { single { getDatabaseHelper() } single { MapSettings() } single { SettingsSource(get()) } single { SettingsRepository(get()) } single { MoneyRepository(get()) } single { MoneyFlowErrorMapper() } single { object : BiometricAvailabilityChecker { override fun isBiometricSupported(): Boolean = true } } viewModel { HomeViewModel(get(), get(), get()) } viewModel { AddTransactionViewModel(get(), get()) } viewModel { CategoriesViewModel(get(), get()) } viewModel { AllTransactionsViewModel(get(), get()) } viewModel { SettingsViewModel(get()) } }, ) } } @After fun teardownResources() { stopKoin() closeDriver() } @Test fun captureMoneyFlowNavHost() { composeRule.setContent { MoneyFlowTheme { MoneyFlowNavHost() } } capture("money_flow_nav_host") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/RoborazziRule.kt ================================================ package com.prof18.moneyflow import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.rules.ActivityScenarioRule import com.github.takahirom.roborazzi.RoborazziRule internal fun roborazziOf( scenario: AndroidComposeTestRule, ComponentActivity>, captureType: RoborazziRule.CaptureType = RoborazziRule.CaptureType.None, ): RoborazziRule { return RoborazziRule( composeRule = scenario, captureRoot = scenario.onRoot(), options = RoborazziRule.Options( captureType = captureType, ), ) } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/RoborazziTestBase.kt ================================================ package com.prof18.moneyflow import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.rules.ActivityScenarioRule import com.github.takahirom.roborazzi.RoborazziRule import com.github.takahirom.roborazzi.captureRoboImage import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.domain.entities.TransactionTypeUI import com.prof18.moneyflow.presentation.categories.data.CategoryUIData import com.prof18.moneyflow.presentation.model.CategoryIcon import org.junit.After import org.junit.Rule import java.io.File open class RoborazziTestBase( captureType: RoborazziRule.CaptureType = RoborazziRule.CaptureType.None, ) { @get:Rule val composeRule: AndroidComposeTestRule, ComponentActivity> = createAndroidComposeRule() @get:Rule val roborazziRule: RoborazziRule = roborazziOf(composeRule, captureType) private val snapshotDir: File = run { val defaultDir = System.getProperty("user.dir") ?.let { File(it).resolve("image/roborazzi").path } ?: "image/roborazzi" val path = System.getProperty("roborazzi.test.record.dir") ?: defaultDir File(path) }.also { directory -> directory.mkdirs() } @After fun tearDown() { composeRule.activityRule.scenario.recreate() } protected fun capture(name: String) { val target = snapshotDir.resolve("$name.png") composeRule.waitForIdle() composeRule.onRoot().captureRoboImage(target.path) } } internal object RoborazziSampleData { val sampleCurrencyConfig = CurrencyConfig( code = "EUR", symbol = "€", decimalPlaces = 2, ) val sampleCategory = CategoryUIData( id = 1, name = "Food", icon = CategoryIcon.IC_HAMBURGER_SOLID, ) val sampleTransactions = listOf( MoneyTransaction( id = 0, title = "Ice Cream", icon = CategoryIcon.IC_ICE_CREAM_SOLID, amountCents = 1_000, type = TransactionTypeUI.EXPENSE, milliseconds = 0, formattedDate = "12 July 2021", ), MoneyTransaction( id = 1, title = "Tip", icon = CategoryIcon.IC_MONEY_CHECK_ALT_SOLID, amountCents = 5_000, type = TransactionTypeUI.INCOME, milliseconds = 0, formattedDate = "12 July 2021", ), ) } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/SettingsRoborazziTest.kt ================================================ package com.prof18.moneyflow import androidx.compose.material3.Scaffold import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.presentation.settings.SettingsScreen import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config( sdk = [33], qualifiers = RobolectricDeviceQualifiers.Pixel7Pro, ) class SettingsRoborazziTest : RoborazziTestBase() { @Test fun captureSettingsScreen() { composeRule.setContent { MoneyFlowTheme { Scaffold { SettingsScreen( biometricAvailabilityChecker = object : BiometricAvailabilityChecker { override fun isBiometricSupported(): Boolean = true }, biometricState = true, onBiometricEnabled = {}, hideSensitiveDataState = true, onHideSensitiveDataEnabled = {}, paddingValues = it, ) } } } capture("settings_screen") } } ================================================ FILE: shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/utilities/TestUtilsAndroid.kt ================================================ package com.prof18.moneyflow.utilities import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.prof18.moneyflow.database.DatabaseHelper import com.prof18.moneyflow.db.MoneyFlowDB import kotlinx.coroutines.Dispatchers internal actual fun createDriver() { val jdbcDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) MoneyFlowDB.Schema.create(jdbcDriver) databaseHelper = DatabaseHelper(jdbcDriver, Dispatchers.Unconfined) driver = jdbcDriver } internal actual fun closeDriver() { driver?.close() databaseHelper = null driver = null } internal actual fun getDatabaseHelper(): DatabaseHelper = requireNotNull(databaseHelper) private var driver: app.cash.sqldelight.db.SqlDriver? = null private var databaseHelper: DatabaseHelper? = null ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_address_book.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_address_card.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_adjust_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_air_freshener_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_algolia.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_allergies_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_ambulance_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_anchor_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_android.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_angle_down_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_angle_left_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_angle_right_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_angle_up_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_apple.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_apple_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_archive_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_archway_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_down_rotate.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_down_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_left_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_right_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_up_rotate.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_arrow_up_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_asterisk_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_at_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_atlas_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_atom_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_award_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_baby_carriage_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bacon_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_balance_scale_left_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_band_aid_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_baseball_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_basketball_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bath_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_battery_three_quarters_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bed_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_beer_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bell.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bell_slash.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bicycle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_biking_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_binoculars_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_birthday_cake_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bitcoin.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_black_tie.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_blender_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_blind_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bolt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bomb_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bone_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bong_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_book_open_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_book_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bookmark.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bowling_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_box_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_brain_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bread_slice_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_briefcase_medical_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_briefcase_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_broadcast_tower_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_broom_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_brush_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bug_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_building.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bullhorn_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bullseye_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_burn_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_bus_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_calculator_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_calendar.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_camera_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_campground_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_candy_cane_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_capsules_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_car_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_car_side_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_caret_down_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_caret_left_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_caret_right_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_caret_up_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_carrot_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cart_arrow_down_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cash_register_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cat_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_certificate_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chair_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chalkboard_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chalkboard_teacher_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_charging_station_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chart_area_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chart_bar.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chart_line_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_chart_pie_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_check_circle.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cheese_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_church_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_city_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_clinic_medical_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_clipboard.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_clock.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cloud_download_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cloud_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cloud_upload_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cocktail_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_code_branch_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_code_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_coffee_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cog_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_coins_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_comment_alt.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_compact_disc_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_compass.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_concierge_bell_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cookie_bite_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_couch_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_credit_card.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_crown_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cubes_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_cut_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_desktop_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_diaspora.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dice_d6_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dna_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dog_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dollar_sign.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dollar_sign_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dolly_flatbed_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dolly_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_donate_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_drafting_compass_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_drum_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_drumstick_bite_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dumbbell_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_dumpster_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_edit.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_egg_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_envelope.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_envelope_open.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_eraser_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_euro_sign.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_euro_sign_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_exchange_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_exclamation_circle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_exclamation_triangle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_expeditedssl.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_external_link_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_eye_dropper_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fan_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fax_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_feather_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_female_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fighter_jet_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_alt.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_audio.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_code.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_csv_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_export_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_import_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_invoice_dollar_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_invoice_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_file_pdf.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fill_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_film_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fire_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fire_extinguisher_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_first_aid_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fish_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_flag.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_flag_checkered_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_flask_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fly.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_folder.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_football_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_fort_awesome.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_frown.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_futbol.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_gamepad_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_gas_pump_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_gavel_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_gift_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_glass_cheers_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_glass_martini_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_globe_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_golf_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_gopuram_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_graduation_cap_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_guitar_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hamburger_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hammer_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hat_cowboy_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hdd.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_headphones_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_helicopter_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_highlighter_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hiking_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_home_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_horse_head_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hospital.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hotdog_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_hourglass_half_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_ice_cream_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_id_card.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_image.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_inbox_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_industry_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_itunes_note.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_key_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_keyboard.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_landmark_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_laptop_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_lightbulb.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_list_ul_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_luggage_cart_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mail_bulk_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_male_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_map_marked_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_marker_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mars_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mask_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_medal_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_medapps.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_medkit_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mercury_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_microchip_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_microphone_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_microscope_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mobile_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_money_bill_wave.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_money_check_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mortar_pestle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_motorcycle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mountain_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_mug_hot_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_oil_can_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pager_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_paint_roller_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_paperclip_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_parachute_box_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_parking_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_passport_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_paw_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pen_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pen_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_phone_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_photo_video_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_piggy_bank_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pills_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pizza_slice_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_plane_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_plug_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pound_sign.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_pound_sign_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_prescription_bottle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_print_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_question_circle.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_readme.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_recycle_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_restroom_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_road_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_robot_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_rocket_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_running_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_screwdriver_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_scroll_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_seedling_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_server_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_shield_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_ship_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_shipping_fast_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_shopping_bag_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_shopping_cart_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_shuttle_van_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_signal_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_sim_card_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_skating_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_skiing_nordic_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_skiing_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_smoking_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_sms_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_snowboarding_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_snowflake.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_socks_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_spider_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_spray_can_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_stamp_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_star_of_life_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_stethoscope_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_sticky_note.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_stopwatch_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_store_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_subway_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_suitcase_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_swimmer_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_syringe_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_table_tennis_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tablet_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tachometer_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tag_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_taxi_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_temperature_high_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_terminal_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_theater_masks_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_thermometer_full_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_ticket_alt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tint_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_toilet_paper_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_toolbox_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tools_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tooth_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tractor_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_train_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_trash_alt.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tree_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_trophy_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_truck_loading_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_truck_moving_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_truck_pickup_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tshirt_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_tv_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_university_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_user.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_user_friends_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_utensils_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_venus_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_vial_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_video_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_volleyball_ball_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_volume_up_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_walking_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_wallet_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_wine_glass_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_wrench_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/drawable/ic_yen_sign_solid.xml ================================================ ================================================ FILE: shared/src/commonMain/composeResources/values/strings.xml ================================================ MoneyFlow Income Expense Select Category Select a date Confirm Cancel Today Description Home Recap Budget Settings Add Transaction Categories Latest Transactions Total Balance Up arrow Down arrow My Wallet Your wallet is empty ¯\\_(ツ)_/¯ Delete Something wrong here! icon Save Add Import Database Export Database Database import completed Database export completed Retry Authenticating... Something wrong with authentication Failed to authenticate. Is that you? Security Face or fingerprint protection Database Management Show sensitive data Hide sensitive data All Transactions Error code: %s Sorry, I\'m not able to add the transaction I can\'t delete the transaction right now 😞 Please retry later. Sorry, an error occurred while retrieving your transactions. Please retry later. Sorry, an error occurred while retrieving your categories Sorry, I\'m not able to compute your Money Summary right now 🧐 Ops, something unexpected happened 😭 Transaction Type Close The amount cannot be empty! Something unexpected happened during the export of the database Something unexpected happened during the import of the database I can\'t find the database file. That is weird! ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/MainViewModel.kt ================================================ package com.prof18.moneyflow import androidx.lifecycle.ViewModel import com.prof18.moneyflow.data.SettingsRepository import com.prof18.moneyflow.features.authentication.BiometricAuthenticator import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.presentation.auth.AuthState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow public class MainViewModel internal constructor( private val settingsRepository: SettingsRepository, private val biometricAvailabilityChecker: BiometricAvailabilityChecker, ) : ViewModel() { private val _authState = MutableStateFlow(initialState()) internal val authState: StateFlow = _authState fun performAuthentication(biometricAuthenticator: BiometricAuthenticator) { if (!shouldUseBiometrics(biometricAuthenticator)) { _authState.value = AuthState.AUTHENTICATED return } _authState.value = AuthState.AUTH_IN_PROGRESS biometricAuthenticator.authenticate( onSuccess = { _authState.value = AuthState.AUTHENTICATED }, onFailure = { _authState.value = AuthState.NOT_AUTHENTICATED }, onError = { _authState.value = AuthState.AUTH_ERROR }, ) } fun lockIfNeeded(biometricAuthenticator: BiometricAuthenticator) { if (shouldUseBiometrics(biometricAuthenticator)) { _authState.value = AuthState.NOT_AUTHENTICATED } } private fun initialState(): AuthState { return if (settingsRepository.isBiometricEnabled()) { AuthState.NOT_AUTHENTICATED } else { AuthState.AUTHENTICATED } } private fun shouldUseBiometrics(biometricAuthenticator: BiometricAuthenticator): Boolean { return settingsRepository.isBiometricEnabled() && biometricAuthenticator.canAuthenticate() && biometricAvailabilityChecker.isBiometricSupported() } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/data/MoneyRepository.kt ================================================ package com.prof18.moneyflow.data import com.prof18.moneyflow.database.DatabaseHelper import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.BalanceRecap import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.domain.entities.MoneySummary import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.domain.entities.TransactionTypeUI import com.prof18.moneyflow.presentation.addtransaction.TransactionToSave import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.utils.currentMonthRange import com.prof18.moneyflow.utils.formatDateDayMonthYear import com.prof18.moneyflow.utils.logError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlin.math.abs internal class MoneyRepository( private val dbSource: DatabaseHelper, ) { companion object { const val DEFAULT_PAGE_SIZE = 30L private const val LATEST_TRANSACTIONS_LIMIT = 10L } private val account = dbSource.selectDefaultAccount() private val currentMonthRange = currentMonthRange() private val allTransactions: Flow> = account.flatMapLatest { selectedAccount -> dbSource.selectLatestTransactions( accountId = selectedAccount.id, limit = LATEST_TRANSACTIONS_LIMIT, ) } .catch { throwable -> val error = MoneyFlowError.GetAllTransaction(throwable) throwable.logError(error) emit(emptyList()) } private val allCategories = dbSource.selectAllCategories() fun getMoneySummary(): Flow = combine( getLatestTransactions(), getBalanceRecap(), getCurrencyConfig(), ) { transactions, balanceRecap, currencyConfig -> MoneySummary( balanceRecap = balanceRecap, latestTransactions = transactions, currencyConfig = currencyConfig, ) } fun getBalanceRecap(): Flow { return account.flatMapLatest { selectedAccount -> val monthRange = currentMonthRange val recap = dbSource.selectMonthlyRecap( accountId = selectedAccount.id, monthStartMillis = monthRange.startMillis, monthEndMillis = monthRange.endMillis, ) val balance = dbSource.selectAccountBalance(selectedAccount.id) combine( balance, recap, ) { totalBalanceCents, monthlyRecap -> BalanceRecap( totalBalanceCents = totalBalanceCents, monthlyIncomeCents = monthlyRecap.incomeCents ?: 0L, monthlyExpensesCents = monthlyRecap.outcomeCents ?: 0L, ) } } } fun getLatestTransactions(): Flow> { return allTransactions.map { it.map { transaction -> val transactionTypeUI = when (transaction.type) { TransactionType.INCOME -> TransactionTypeUI.INCOME TransactionType.OUTCOME -> TransactionTypeUI.EXPENSE } val transactionTitle = transaction.description.takeUnless { it.isNullOrEmpty() } ?: transaction.categoryName MoneyTransaction( id = transaction.id, title = transactionTitle, icon = CategoryIcon.fromValue(transaction.iconName), amountCents = transaction.amountCents, type = transactionTypeUI, milliseconds = transaction.dateMillis, formattedDate = transaction.dateMillis.formatDateDayMonthYear(), ) } } } suspend fun insertTransaction(transactionToSave: TransactionToSave) { val accountId = account.first().id val amountCents = abs(transactionToSave.amountCents) dbSource.insertTransaction( accountId = accountId, dateMillis = transactionToSave.dateMillis, amountCents = amountCents, description = transactionToSave.description, categoryId = transactionToSave.categoryId, transactionType = transactionToSave.transactionType, ) } suspend fun deleteTransaction(transactionId: Long) { val transaction = dbSource.getTransaction(transactionId) ?: return dbSource.deleteTransaction(transaction.id) } fun getCategories(): Flow> { return allCategories.map { it.map { category -> Category( id = category.id, name = category.name, icon = CategoryIcon.fromValue(category.iconName), type = category.type, createdAtMillis = category.createdAtMillis, ) } } } suspend fun getTransactionsPaginated( pageNum: Long, pageSize: Long, ): List { val accountId = account.first().id return dbSource.getTransactionsPaginated( accountId = accountId, pageNum = pageNum, pageSize = pageSize, ) .map { transaction -> // TODO: return a different thing, to create date headers val transactionTypeUI = when (transaction.type) { TransactionType.INCOME -> TransactionTypeUI.INCOME TransactionType.OUTCOME -> TransactionTypeUI.EXPENSE } val transactionTitle = transaction.description.takeUnless { it.isNullOrEmpty() } ?: transaction.categoryName MoneyTransaction( id = transaction.id, title = transactionTitle, icon = CategoryIcon.fromValue(transaction.iconName), amountCents = transaction.amountCents, type = transactionTypeUI, milliseconds = transaction.dateMillis, formattedDate = transaction.dateMillis.formatDateDayMonthYear(), ) } } fun getCurrencyConfig(): Flow = account.map { account -> CurrencyConfig( code = account.currencyCode, symbol = account.currencySymbol, decimalPlaces = account.currencyDecimalPlaces.toInt(), ) } suspend fun addCategory( name: String, type: TransactionType, iconName: String, ) { dbSource.insertCategory( name = name, type = type, iconName = iconName, isSystem = false, ) } suspend fun updateCategory(category: Category) { dbSource.updateCategory( id = category.id, name = category.name, iconName = category.icon.iconName, ) } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/data/SettingsRepository.kt ================================================ package com.prof18.moneyflow.data import com.prof18.moneyflow.data.settings.SettingsSource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow internal class SettingsRepository( private val settingsSource: SettingsSource, ) { // Just to avoid getting on disk every time the field is accessed private var isBiometricEnabled: Boolean? = null val hideSensibleDataState: StateFlow get() = _hideSensibleDataState private var _hideSensibleDataState = MutableStateFlow(false) init { _hideSensibleDataState.value = settingsSource.getHideSensitiveData() } fun isBiometricEnabled(): Boolean { if (isBiometricEnabled == null) { isBiometricEnabled = settingsSource.getUseBiometric() } return isBiometricEnabled!! } fun setBiometric(enabled: Boolean) { settingsSource.setUseBiometric(enabled) isBiometricEnabled = enabled } fun setHideSensitiveData(hide: Boolean) { settingsSource.setHideSensitiveData(hide) _hideSensibleDataState.value = hide } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/data/settings/SettingsSource.kt ================================================ package com.prof18.moneyflow.data.settings import com.russhwolf.settings.Settings import com.russhwolf.settings.set internal enum class SettingsFields { USE_BIOMETRIC, HIDE_SENSITIVE_DATA, } internal class SettingsSource(private val settings: Settings) { fun getUseBiometric(): Boolean = settings.getBoolean(SettingsFields.USE_BIOMETRIC.name, false) fun setUseBiometric(value: Boolean) = settings.set(SettingsFields.USE_BIOMETRIC.name, value) fun getHideSensitiveData(): Boolean = settings.getBoolean(SettingsFields.HIDE_SENSITIVE_DATA.name, false) fun setHideSensitiveData(value: Boolean) = settings.set(SettingsFields.HIDE_SENSITIVE_DATA.name, value) fun clear() = settings.clear() } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/database/DatabaseHelper.kt ================================================ package com.prof18.moneyflow.database import app.cash.sqldelight.EnumColumnAdapter import app.cash.sqldelight.Transacter import app.cash.sqldelight.TransactionWithoutReturn import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOneOrDefault import app.cash.sqldelight.db.SqlDriver import com.prof18.moneyflow.database.default.defaultCategories import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.db.AccountTable import com.prof18.moneyflow.db.CategoryTable import com.prof18.moneyflow.db.MoneyFlowDB import com.prof18.moneyflow.db.SelectLatestTransactions import com.prof18.moneyflow.db.SelectMonthlyRecap import com.prof18.moneyflow.db.SelectTransactionsPaginated import com.prof18.moneyflow.db.TransactionTable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext import kotlin.time.Clock internal class DatabaseHelper( sqlDriver: SqlDriver, dispatcher: CoroutineDispatcher? = null, ) { private val backgroundDispatcher: CoroutineDispatcher = dispatcher ?: Dispatchers.Default private val dbRef: MoneyFlowDB = MoneyFlowDB( driver = sqlDriver, CategoryTableAdapter = CategoryTable.Adapter( typeAdapter = EnumColumnAdapter(), ), TransactionTableAdapter = TransactionTable.Adapter( typeAdapter = EnumColumnAdapter(), ), ) init { seedDefaultsIfNeeded() } fun selectLatestTransactions( accountId: Long, limit: Long, ): Flow> = dbRef.transactionTableQueries .selectLatestTransactions( accountId = accountId, limit = limit, ) .asFlow() .mapToList(backgroundDispatcher) .flowOn(backgroundDispatcher) fun selectAllCategories(): Flow> = dbRef.categoryTableQueries .selectAll() .asFlow() .mapToList(backgroundDispatcher) .flowOn(backgroundDispatcher) fun selectCategoriesByType(type: TransactionType): Flow> = dbRef.categoryTableQueries .selectByType(type) .asFlow() .mapToList(backgroundDispatcher) .flowOn(backgroundDispatcher) fun selectDefaultAccount(): Flow = dbRef.accountTableQueries .selectDefaultAccount() .asFlow() .mapToOneOrDefault( AccountTable( id = 1.toLong(), name = "Default Account", currencyCode = "EUR", currencySymbol = "€", currencyDecimalPlaces = 2.toLong(), isDefault = 1.toLong(), createdAtMillis = Clock.System.now().toEpochMilliseconds(), ), backgroundDispatcher, ) .flowOn(backgroundDispatcher) fun selectAccountBalance(accountId: Long): Flow = dbRef.transactionTableQueries .selectAccountBalance(accountId) .asFlow() .mapToOneOrDefault(0L, backgroundDispatcher) .flowOn(backgroundDispatcher) fun selectMonthlyRecap( accountId: Long, monthStartMillis: Long, monthEndMillis: Long, ): Flow = dbRef.transactionTableQueries .selectMonthlyRecap(accountId, monthStartMillis, monthEndMillis) .asFlow() .mapToOneOrDefault( SelectMonthlyRecap( incomeCents = 0L, outcomeCents = 0L, ), backgroundDispatcher, ) .flowOn(backgroundDispatcher) suspend fun insertTransaction( accountId: Long, dateMillis: Long, amountCents: Long, description: String?, categoryId: Long, transactionType: TransactionType, ) { val createdAtMillis = Clock.System.now().toEpochMilliseconds() dbRef.transactionWithContext(backgroundDispatcher) { dbRef.transactionTableQueries.insertTransaction( accountId = accountId, dateMillis = dateMillis, amountCents = amountCents, description = description, categoryId = categoryId, type = transactionType, createdAtMillis = createdAtMillis, ) } } suspend fun deleteTransaction( transactionId: Long, ) { dbRef.transactionWithContext(backgroundDispatcher) { dbRef.transactionTableQueries.deleteTransaction(transactionId) } } suspend fun getTransaction(transactionId: Long): TransactionTable? = withContext(backgroundDispatcher) { return@withContext dbRef.transactionTableQueries.selectTransaction(transactionId) .executeAsOneOrNull() } suspend fun getTransactionsPaginated( accountId: Long, pageNum: Long, pageSize: Long, ): List = withContext(backgroundDispatcher) { val offset = pageNum * pageSize return@withContext dbRef.transactionTableQueries .selectTransactionsPaginated( accountId = accountId, pageSize = pageSize, offset = offset, ) .executeAsList() } suspend fun insertCategory( name: String, type: TransactionType, iconName: String, isSystem: Boolean, createdAtMillis: Long = Clock.System.now().toEpochMilliseconds(), ) = withContext(backgroundDispatcher) { dbRef.categoryTableQueries.insertCategory( name = name, type = type, iconName = iconName, isSystem = if (isSystem) 1L else 0L, createdAtMillis = createdAtMillis, ) } suspend fun updateCategory( id: Long, name: String, iconName: String, ) = withContext(backgroundDispatcher) { dbRef.categoryTableQueries.updateCategory( id = id, name = name, iconName = iconName, ) } suspend fun countTransactionsForCategory(categoryId: Long): Long = withContext(backgroundDispatcher) { return@withContext dbRef.categoryTableQueries.countTransactionsForCategory(categoryId) .executeAsOne() } private fun seedDefaultsIfNeeded() = runBlocking(backgroundDispatcher) { val account = dbRef.accountTableQueries.selectDefaultAccount().executeAsOneOrNull() if (account == null) { dbRef.accountTableQueries.insertAccount( name = "Default Account", currencyCode = "EUR", currencySymbol = "€", currencyDecimalPlaces = 2.toLong(), isDefault = 1.toLong(), createdAtMillis = Clock.System.now().toEpochMilliseconds(), ) } if (dbRef.categoryTableQueries.selectAll().executeAsList().isEmpty()) { dbRef.transactionWithContext(backgroundDispatcher) { defaultCategories.forEach { category -> dbRef.categoryTableQueries.insertCategory( name = category.name, type = category.type, iconName = category.iconName, isSystem = 1.toLong(), createdAtMillis = category.createdAtMillis, ) } } } } private suspend fun Transacter.transactionWithContext( coroutineContext: CoroutineContext, noEnclosing: Boolean = false, body: TransactionWithoutReturn.() -> Unit, ) { withContext(coroutineContext) { this@transactionWithContext.transaction(noEnclosing) { body() } } } companion object { const val DB_FILE_NAME_WITH_EXTENSION = "MoneyFlow.db" const val DB_FILE_NAME = "MoneyFlow" const val APP_DATABASE_NAME_PROD = "MoneyFlowDB" const val APP_DATABASE_NAME_DEBUG = "MoneyFlowDB-debug" const val DATABASE_NAME = APP_DATABASE_NAME_PROD } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/database/default/DefaultValues.kt ================================================ package com.prof18.moneyflow.database.default import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.presentation.model.CategoryIcon import kotlin.time.Clock internal data class DefaultCategory( val name: String, val type: TransactionType, val iconName: String, val createdAtMillis: Long, ) private val seedTimestamp = Clock.System.now().toEpochMilliseconds() // TODO: localize and set category id directly instead of the name internal val defaultCategories = listOf( DefaultCategory( name = "Accessories", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BLACK_TIE.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Bar", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_COCKTAIL_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Books", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BOOK_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Clothing", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_TSHIRT_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Eating Out", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_HAMBURGER_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Education", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_GRADUATION_CAP_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Electronics", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_LAPTOP_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Entertainment", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BOWLING_BALL_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Extra Income", type = TransactionType.INCOME, iconName = CategoryIcon.IC_DONATE_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Family", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_HOME_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Fees", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_FILE_INVOICE_DOLLAR_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Film", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_FILM_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Food & Beverage", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_DRUMSTICK_BITE_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Footwear", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_SOCKS_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Gifts", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_GIFT_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Hairdresser", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_CUT_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Hotel", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BUILDING.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Internet Service", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_SERVER_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Love and Friends", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_USER_FRIENDS_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Medical", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_HOSPITAL.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Music", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_HEADPHONES_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Other", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_DOLLAR_SIGN.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Parking Fees", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_PARKING_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Petrol", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_CHARGING_STATION_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Phone", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_PHONE_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Plane", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_PLANE_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Salary", type = TransactionType.INCOME, iconName = CategoryIcon.IC_MONEY_CHECK_ALT_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Selling", type = TransactionType.INCOME, iconName = CategoryIcon.IC_DOLLY_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Software", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_FILE_CODE.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Sport", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BASKETBALL_BALL_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Train", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_TRAIN_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Transportation", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_BUS_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Travel", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_GLOBE_SOLID.iconName, createdAtMillis = seedTimestamp, ), DefaultCategory( name = "Videogames", type = TransactionType.OUTCOME, iconName = CategoryIcon.IC_GAMEPAD_SOLID.iconName, createdAtMillis = seedTimestamp, ), ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/database/model/TransactionType.kt ================================================ package com.prof18.moneyflow.database.model enum class TransactionType { INCOME, OUTCOME, } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/di/Koin.kt ================================================ package com.prof18.moneyflow.di import com.prof18.moneyflow.MainViewModel import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.data.SettingsRepository import com.prof18.moneyflow.data.settings.SettingsSource import com.prof18.moneyflow.database.DatabaseHelper import com.prof18.moneyflow.features.addtransaction.AddTransactionViewModel import com.prof18.moneyflow.features.alltransactions.AllTransactionsViewModel import com.prof18.moneyflow.features.categories.CategoriesViewModel import com.prof18.moneyflow.features.home.HomeViewModel import com.prof18.moneyflow.features.settings.SettingsViewModel import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.utils.DispatcherProvider import kotlinx.coroutines.Dispatchers import org.koin.core.KoinApplication import org.koin.core.context.startKoin import org.koin.core.module.Module import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module public fun initKoin(additionalModules: List): KoinApplication { return startKoin { modules(additionalModules + platformModule + coreModule) } } private val coreModule = module { single { DatabaseHelper(get(), Dispatchers.Default) } single { SettingsSource(get()) } single { MoneyFlowErrorMapper() } factory { DispatcherProvider() } // Repository single { SettingsRepository(get()) } single { MoneyRepository(get()) } viewModelOf(::MainViewModel) viewModelOf(::HomeViewModel) viewModelOf(::AddTransactionViewModel) viewModelOf(::CategoriesViewModel) viewModelOf(::SettingsViewModel) viewModelOf(::AllTransactionsViewModel) } internal expect val platformModule: Module ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/BalanceRecap.kt ================================================ package com.prof18.moneyflow.domain.entities internal data class BalanceRecap( val totalBalanceCents: Long, val monthlyIncomeCents: Long, val monthlyExpensesCents: Long, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/Category.kt ================================================ package com.prof18.moneyflow.domain.entities import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.presentation.model.CategoryIcon internal data class Category( val id: Long, val name: String, val icon: CategoryIcon, // TODO: delete? val type: TransactionType, val createdAtMillis: Long, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/CurrencyConfig.kt ================================================ package com.prof18.moneyflow.domain.entities internal data class CurrencyConfig( val code: String, val symbol: String, val decimalPlaces: Int, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/DBImportExportException.kt ================================================ package com.prof18.moneyflow.domain.entities internal class DatabaseExportException : Exception() internal class DatabaseImportException : Exception() ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/MoneyFlowError.kt ================================================ package com.prof18.moneyflow.domain.entities @Suppress("MagicNumber") internal sealed class MoneyFlowError(val code: Int, val throwable: Throwable) { class GetMoneySummary(throwable: Throwable) : MoneyFlowError(100, throwable) class DeleteTransaction(throwable: Throwable) : MoneyFlowError(101, throwable) class AddTransaction(throwable: Throwable) : MoneyFlowError(102, throwable) class GetAllTransaction(throwable: Throwable) : MoneyFlowError(103, throwable) class GetCategories(throwable: Throwable) : MoneyFlowError(104, throwable) class DatabaseExport(throwable: Throwable) : MoneyFlowError(105, throwable) class DatabaseImport(throwable: Throwable) : MoneyFlowError(106, throwable) class DatabaseNotFound(throwable: Throwable) : MoneyFlowError(107, throwable) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/MoneyFlowResult.kt ================================================ package com.prof18.moneyflow.domain.entities import com.prof18.moneyflow.presentation.model.UIErrorMessage internal sealed class MoneyFlowResult { data class Success(val data: T) : MoneyFlowResult() data class Error(val uiErrorMessage: UIErrorMessage) : MoneyFlowResult() } internal fun MoneyFlowResult.doOnError( onError: (UIErrorMessage) -> Unit, ) { if (this is MoneyFlowResult.Error) { onError(this.uiErrorMessage) } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/MoneySummary.kt ================================================ package com.prof18.moneyflow.domain.entities internal data class MoneySummary( val balanceRecap: BalanceRecap, val latestTransactions: List, val currencyConfig: CurrencyConfig, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/MoneyTransaction.kt ================================================ package com.prof18.moneyflow.domain.entities import com.prof18.moneyflow.presentation.model.CategoryIcon internal data class MoneyTransaction( val id: Long, val title: String, val icon: CategoryIcon, val amountCents: Long, val type: TransactionTypeUI, val milliseconds: Long, val formattedDate: String, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/domain/entities/TransactionTypeUI.kt ================================================ package com.prof18.moneyflow.domain.entities internal enum class TransactionTypeUI { INCOME, EXPENSE, } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/addtransaction/AddTransactionViewModel.kt ================================================ package com.prof18.moneyflow.features.addtransaction import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.domain.entities.MoneyFlowResult import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.presentation.addtransaction.AddTransactionAction import com.prof18.moneyflow.presentation.addtransaction.TransactionToSave import com.prof18.moneyflow.presentation.model.UIErrorMessage import com.prof18.moneyflow.utils.formatDateDayMonthYear import com.prof18.moneyflow.utils.logError import com.prof18.moneyflow.utils.toAmountCents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.amount_not_empty_error import kotlin.time.Clock internal class AddTransactionViewModel( private val moneyRepository: MoneyRepository, private val errorMapper: MoneyFlowErrorMapper, ) : ViewModel() { private val initialSelectedDateMillis: Long = Clock.System.now().toEpochMilliseconds() private val _uiState = MutableStateFlow( AddTransactionUiState( selectedTransactionType = TransactionType.INCOME, amountText = "", descriptionText = null, dateLabel = initialSelectedDateMillis.formatDateDayMonthYear(), addTransactionAction = null, currencyConfig = null, selectedDateMillis = initialSelectedDateMillis, ), ) val uiState: StateFlow = _uiState init { observeCurrencyConfig() } private fun observeCurrencyConfig() { viewModelScope.launch { moneyRepository.getCurrencyConfig().collectLatest { config -> _uiState.update { state -> state.copy(currencyConfig = config) } } } } fun updateSelectedDate(selectedDateMillis: Long) { _uiState.update { state -> state.copy( dateLabel = selectedDateMillis.formatDateDayMonthYear(), selectedDateMillis = selectedDateMillis, ) } } fun addTransaction(categoryId: Long) { val currencyConfig = uiState.value.currencyConfig ?: CurrencyConfig("EUR", "€", 2) val amountCents = uiState.value.amountText.toAmountCents(currencyConfig) if (amountCents == null) { val errorMessage = UIErrorMessage( message = Res.string.amount_not_empty_error, ) _uiState.update { state -> state.copy(addTransactionAction = AddTransactionAction.ShowError(errorMessage)) } return } viewModelScope.launch { val result = try { moneyRepository.insertTransaction( TransactionToSave( dateMillis = uiState.value.selectedDateMillis, amountCents = amountCents, description = uiState.value.descriptionText, categoryId = categoryId, transactionType = uiState.value.selectedTransactionType, ), ) MoneyFlowResult.Success(Unit) } catch (throwable: Throwable) { val error = MoneyFlowError.AddTransaction(throwable) throwable.logError(error) MoneyFlowResult.Error(errorMapper.getUIErrorMessage(error)) } _uiState.update { state -> val action = when (result) { is MoneyFlowResult.Success -> AddTransactionAction.GoBack is MoneyFlowResult.Error -> AddTransactionAction.ShowError(result.uiErrorMessage) } state.copy(addTransactionAction = action) } } } fun resetAction() { _uiState.update { state -> state.copy(addTransactionAction = null) } } fun updateAmountText(amountText: String) { _uiState.update { state -> state.copy(amountText = amountText) } } fun updateDescriptionText(description: String?) { _uiState.update { state -> state.copy(descriptionText = description) } } fun updateTransactionType(transactionType: TransactionType) { _uiState.update { state -> state.copy(selectedTransactionType = transactionType) } } } internal data class AddTransactionUiState( val selectedTransactionType: TransactionType, val amountText: String, val descriptionText: String?, val dateLabel: String?, val addTransactionAction: AddTransactionAction?, val currencyConfig: CurrencyConfig?, val selectedDateMillis: Long, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/alltransactions/AllTransactionsViewModel.kt ================================================ package com.prof18.moneyflow.features.alltransactions import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.presentation.model.UIErrorMessage import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class AllTransactionsViewModel( private val moneyRepository: MoneyRepository, private val errorMapper: MoneyFlowErrorMapper, ) : ViewModel() { private var currentPage: Long = 0 // TODO: migrate to a loading/error state like ICE private val _state = MutableStateFlow( AllTransactionsUiState( currencyConfig = CurrencyConfig(code = "", symbol = "", decimalPlaces = 2), ), ) val state: StateFlow = _state init { loadInitialPage() } private fun loadInitialPage() { viewModelScope.launch { _state.update { it.copy( isLoading = true, error = null, transactions = emptyList(), endReached = false, isLoadingMore = false, ) } val currencyConfigDeferred = async { moneyRepository.getCurrencyConfig().first() } val firstPageDeferred = async { moneyRepository.getTransactionsPaginated( pageNum = currentPage, pageSize = MoneyRepository.DEFAULT_PAGE_SIZE, ) } runCatching { val currencyConfig = currencyConfigDeferred.await() val firstPage = firstPageDeferred.await() val endReached = firstPage.size < MoneyRepository.DEFAULT_PAGE_SIZE currentPage += 1 _state.update { state -> state.copy( currencyConfig = currencyConfig, transactions = firstPage, isLoading = false, endReached = endReached, ) } }.onFailure { throwable -> val error = MoneyFlowError.GetAllTransaction(throwable) val uiError = errorMapper.getUIErrorMessage(error) _state.update { state -> state.copy(isLoading = false, error = uiError) } } } } fun mapErrorToErrorMessage(error: MoneyFlowError): UIErrorMessage { return errorMapper.getUIErrorMessage(error) } fun loadNextPage(reset: Boolean = false) { if (_state.value.isLoadingMore || _state.value.endReached || _state.value.currencyConfig.code.isEmpty()) return viewModelScope.launch { _state.update { state -> if (reset) { currentPage = 0 state.copy(isLoading = true, transactions = emptyList(), error = null, endReached = false) } else { state.copy(isLoadingMore = true, error = null) } } try { val data = moneyRepository.getTransactionsPaginated( pageNum = currentPage, pageSize = MoneyRepository.DEFAULT_PAGE_SIZE, ) val endReached = data.size < MoneyRepository.DEFAULT_PAGE_SIZE currentPage += 1 _state.update { state -> state.copy( transactions = state.transactions + data, isLoading = false, isLoadingMore = false, endReached = endReached, ) } } catch (throwable: Throwable) { val error = MoneyFlowError.GetAllTransaction(throwable) val uiError = errorMapper.getUIErrorMessage(error) _state.update { state -> state.copy( isLoading = false, isLoadingMore = false, error = uiError, ) } } } } } internal data class AllTransactionsUiState( val transactions: List = emptyList(), val isLoading: Boolean = false, val isLoadingMore: Boolean = false, val error: UIErrorMessage? = null, val endReached: Boolean = false, val currencyConfig: CurrencyConfig, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/authentication/BiometricAuthenticator.kt ================================================ package com.prof18.moneyflow.features.authentication public interface BiometricAuthenticator { fun canAuthenticate(): Boolean fun authenticate( onSuccess: () -> Unit, onFailure: () -> Unit, onError: () -> Unit, ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/categories/CategoriesViewModel.kt ================================================ package com.prof18.moneyflow.features.categories import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.presentation.categories.CategoryModel import com.prof18.moneyflow.utils.logError import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn internal class CategoriesViewModel( private val moneyRepository: MoneyRepository, private val errorMapper: MoneyFlowErrorMapper, ) : ViewModel() { val categories: StateFlow = moneyRepository.getCategories() .map { CategoryModel.CategoryState(it) as CategoryModel } .catch { throwable: Throwable -> val error = MoneyFlowError.GetCategories(throwable) throwable.logError(error) emit(CategoryModel.Error(errorMapper.getUIErrorMessage(error))) }.stateIn( scope = viewModelScope, started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(5_000), initialValue = CategoryModel.Loading, ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/home/HomeViewModel.kt ================================================ package com.prof18.moneyflow.features.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.moneyflow.data.MoneyRepository import com.prof18.moneyflow.data.SettingsRepository import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.domain.entities.MoneyFlowResult import com.prof18.moneyflow.presentation.MoneyFlowErrorMapper import com.prof18.moneyflow.presentation.home.HomeModel import com.prof18.moneyflow.utils.logError import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class HomeViewModel( private val moneyRepository: MoneyRepository, private val settingsRepository: SettingsRepository, private val errorMapper: MoneyFlowErrorMapper, ) : ViewModel() { private val _homeModel = MutableStateFlow(HomeModel.Loading) val homeModel: StateFlow = _homeModel val hideSensitiveDataState: StateFlow = settingsRepository.hideSensibleDataState init { observeHomeModel() } private fun observeHomeModel() { viewModelScope.launch { moneyRepository.getMoneySummary() .map { summary -> HomeModel.HomeState( balanceRecap = summary.balanceRecap, latestTransactions = summary.latestTransactions, currencyConfig = summary.currencyConfig, ) as HomeModel } .catch { throwable: Throwable -> val error = MoneyFlowError.GetMoneySummary(throwable) throwable.logError(error) val errorMessage = errorMapper.getUIErrorMessage(error) emit(HomeModel.Error(errorMessage)) } .collect { model -> _homeModel.value = model } } } fun changeSensitiveDataVisibility(status: Boolean) = settingsRepository.setHideSensitiveData(status) fun deleteTransaction(id: Long) { viewModelScope.launch { val result = runCatching { moneyRepository.deleteTransaction(id) } .fold( onSuccess = { MoneyFlowResult.Success(Unit) }, onFailure = { val error = MoneyFlowError.DeleteTransaction(it) it.logError(error) MoneyFlowResult.Error(errorMapper.getUIErrorMessage(error)) }, ) if (result is MoneyFlowResult.Error) { _homeModel.update { HomeModel.Error(result.uiErrorMessage) } } } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/settings/BiometricAvailabilityChecker.kt ================================================ package com.prof18.moneyflow.features.settings internal interface BiometricAvailabilityChecker { fun isBiometricSupported(): Boolean } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/features/settings/SettingsViewModel.kt ================================================ package com.prof18.moneyflow.features.settings import androidx.lifecycle.ViewModel import com.prof18.moneyflow.data.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update internal class SettingsViewModel( private val settingsRepository: SettingsRepository, ) : ViewModel() { private val _biometricState = MutableStateFlow(false) val biometricState: StateFlow = _biometricState val hideSensitiveDataState: StateFlow = settingsRepository.hideSensibleDataState init { _biometricState.value = settingsRepository.isBiometricEnabled() } fun updateBiometricState(enabled: Boolean) { settingsRepository.setBiometric(enabled) _biometricState.update { enabled } } fun updateHideSensitiveDataState(enabled: Boolean) { settingsRepository.setHideSensitiveData(enabled) } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/AppRoute.kt ================================================ package com.prof18.moneyflow.navigation import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable internal sealed interface AppRoute @Serializable @SerialName("home") internal data object HomeRoute : AppRoute @Serializable @SerialName("settings") internal data object SettingsRoute : AppRoute @Serializable @SerialName("add_transaction") internal data object AddTransactionRoute : AppRoute @Serializable @SerialName("categories") internal data class CategoriesRoute(val fromAddTransaction: Boolean) : AppRoute @Serializable @SerialName("all_transactions") internal data object AllTransactionsRoute : AppRoute ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/MoneyFlowNavHost.kt ================================================ package com.prof18.moneyflow.navigation import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSerializable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer import com.prof18.moneyflow.features.addtransaction.AddTransactionViewModel import com.prof18.moneyflow.features.alltransactions.AllTransactionsViewModel import com.prof18.moneyflow.features.categories.CategoriesViewModel import com.prof18.moneyflow.features.home.HomeViewModel import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.features.settings.SettingsViewModel import com.prof18.moneyflow.presentation.addtransaction.AddTransactionScreen import com.prof18.moneyflow.presentation.alltransactions.AllTransactionsScreen import com.prof18.moneyflow.presentation.categories.CategoriesScreen import com.prof18.moneyflow.presentation.categories.data.CategoryUIData import com.prof18.moneyflow.presentation.home.HomeScreen import com.prof18.moneyflow.presentation.settings.SettingsScreen import com.prof18.moneyflow.utils.LocalAppDensity import com.prof18.moneyflow.utils.LocalAppLocale import com.prof18.moneyflow.utils.LocalAppTheme import com.prof18.moneyflow.utils.customAppDensity import com.prof18.moneyflow.utils.customAppLocale import com.prof18.moneyflow.utils.customAppThemeIsDark import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.home_screen import money_flow.shared.generated.resources.ic_cog_solid import money_flow.shared.generated.resources.ic_home_solid import money_flow.shared.generated.resources.settings_screen import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel private const val DEFAULT_ANIMATION_DURATION_MILLIS = 300 private fun bottomSheetForwardTransition() = slideInVertically( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), initialOffsetY = { it }, ) togetherWith ExitTransition.None private fun bottomSheetPopTransition() = EnterTransition.None togetherWith slideOutVertically( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), targetOffsetY = { it }, ) private val bottomSheetTransitionMetadata = NavDisplay.transitionSpec { bottomSheetForwardTransition() } + NavDisplay.popTransitionSpec { bottomSheetPopTransition() } + NavDisplay.predictivePopTransitionSpec { _: Int -> bottomSheetPopTransition() } @Composable internal fun MoneyFlowNavHost(modifier: Modifier = Modifier) { val backStack = rememberSerializable(serializer = SnapshotStateListSerializer(AppRoute.serializer())) { mutableStateListOf(HomeRoute) } val categoryState = remember { mutableStateOf(null) } CompositionLocalProvider( LocalAppLocale provides customAppLocale, LocalAppTheme provides customAppThemeIsDark, LocalAppDensity provides customAppDensity, ) { key(customAppLocale) { key(customAppThemeIsDark) { key(customAppDensity) { Scaffold( modifier = modifier, contentWindowInsets = WindowInsets.safeDrawing.only( WindowInsetsSides.Top + WindowInsetsSides.Horizontal, ), bottomBar = { BottomBar( currentRoute = backStack.lastOrNull(), onNavigate = { destination -> backStack.clear() backStack.add(destination) }, ) }, ) { paddingValues -> NavDisplay( backStack = backStack, entryProvider = entryProvider { screens(backStack, categoryState, paddingValues) }, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator(), ), transitionSpec = { slideInHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), initialOffsetX = { it }, ) togetherWith slideOutHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), targetOffsetX = { -it }, ) }, popTransitionSpec = { slideInHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), initialOffsetX = { -it }, ) togetherWith slideOutHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), targetOffsetX = { it }, ) }, predictivePopTransitionSpec = { edge -> slideInHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), initialOffsetX = { -it }, ) togetherWith slideOutHorizontally( animationSpec = tween(DEFAULT_ANIMATION_DURATION_MILLIS), targetOffsetX = { it }, ) }, ) } } } } } } @Composable private fun BottomBar( currentRoute: AppRoute?, onNavigate: (AppRoute) -> Unit, ) { if (currentRoute !is HomeRoute && currentRoute !is SettingsRoute) return NavigationBar( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ) { bottomNavigationItems.forEach { item -> NavigationBarItem( icon = { Icon( painter = painterResource(item.drawableRes), contentDescription = null, modifier = Modifier.size(22.dp), ) }, label = { Text(stringResource(item.titleRes)) }, selected = currentRoute::class == item.route::class, colors = NavigationBarItemDefaults.colors( selectedIconColor = MaterialTheme.colorScheme.primary, unselectedIconColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f), selectedTextColor = MaterialTheme.colorScheme.onPrimary, unselectedTextColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f), indicatorColor = MaterialTheme.colorScheme.onPrimary, ), onClick = { if (currentRoute::class != item.route::class) onNavigate(item.route) }, ) } } } private data class BottomNavigationItem( val route: AppRoute, val titleRes: StringResource, val drawableRes: DrawableResource, ) private val bottomNavigationItems = listOf( BottomNavigationItem( route = HomeRoute, titleRes = Res.string.home_screen, drawableRes = Res.drawable.ic_home_solid, ), BottomNavigationItem( route = SettingsRoute, titleRes = Res.string.settings_screen, drawableRes = Res.drawable.ic_cog_solid, ), ) private fun EntryProviderScope.screens( backStack: MutableList, categoryState: MutableState, paddingValues: PaddingValues, ) { entry { val homeViewModel = koinViewModel() val homeModel by homeViewModel.homeModel.collectAsState() val hideSensitiveData by homeViewModel.hideSensitiveDataState.collectAsState() HomeScreen( homeModel = homeModel, hideSensitiveDataState = hideSensitiveData, navigateToAllTransactions = { backStack.add(AllTransactionsRoute) }, paddingValues = paddingValues, navigateToAddTransaction = { backStack.add(AddTransactionRoute) }, deleteTransaction = { id -> homeViewModel.deleteTransaction(id) }, changeSensitiveDataVisibility = { homeViewModel.changeSensitiveDataVisibility(it) }, ) } entry { val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsState() AddTransactionScreen( categoryState = categoryState, navigateUp = { backStack.removeLastOrNull() }, navigateToCategoryList = { backStack.add(CategoriesRoute(fromAddTransaction = true)) }, addTransaction = viewModel::addTransaction, amountText = uiState.amountText, updateAmountText = viewModel::updateAmountText, descriptionText = uiState.descriptionText, updateDescriptionText = viewModel::updateDescriptionText, selectedTransactionType = uiState.selectedTransactionType, updateTransactionType = viewModel::updateTransactionType, updateSelectedDate = viewModel::updateSelectedDate, dateLabel = uiState.dateLabel, selectedDateMillis = uiState.selectedDateMillis, addTransactionAction = uiState.addTransactionAction, resetAction = viewModel::resetAction, currencyConfig = uiState.currencyConfig, ) } entry(metadata = bottomSheetTransitionMetadata) { route -> val viewModel = koinViewModel() val categoryModel by viewModel.categories.collectAsState() CategoriesScreen( navigateUp = { backStack.removeLastOrNull() }, sendCategoryBack = { categoryData -> if (route.fromAddTransaction) { categoryState.value = categoryData backStack.removeLastOrNull() } }, isFromAddTransaction = route.fromAddTransaction, categoryModel = categoryModel, ) } entry(metadata = bottomSheetTransitionMetadata) { val viewModel = koinViewModel() AllTransactionsScreen( stateFlow = viewModel.state, loadNextPage = viewModel::loadNextPage, navigateUp = { backStack.removeLastOrNull() }, ) } entry { val viewModel = koinViewModel() val hideDataState by viewModel.hideSensitiveDataState.collectAsState() val biometricState by viewModel.biometricState.collectAsState() val biometricAvailabilityChecker: BiometricAvailabilityChecker = koinInject() SettingsScreen( biometricAvailabilityChecker = biometricAvailabilityChecker, biometricState = biometricState, onBiometricEnabled = viewModel::updateBiometricState, hideSensitiveDataState = hideDataState, onHideSensitiveDataEnabled = viewModel::updateHideSensitiveDataState, paddingValues = paddingValues, ) } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/MoneyFlowApp.kt ================================================ package com.prof18.moneyflow.presentation import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.prof18.moneyflow.MainViewModel import com.prof18.moneyflow.features.authentication.BiometricAuthenticator import com.prof18.moneyflow.navigation.MoneyFlowNavHost import com.prof18.moneyflow.presentation.auth.AuthScreen import com.prof18.moneyflow.presentation.auth.AuthState import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.koin.compose.viewmodel.koinViewModel @Composable public fun MoneyFlowApp( biometricAuthenticator: BiometricAuthenticator, modifier: Modifier = Modifier, ) { val viewModel = koinViewModel() val authState by viewModel.authState.collectAsState() LaunchedEffect(Unit) { viewModel.performAuthentication(biometricAuthenticator) } MoneyFlowTheme { Box(modifier = modifier.fillMaxSize()) { MoneyFlowNavHost() AnimatedVisibility(visible = authState != AuthState.AUTHENTICATED) { Surface( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing), ) { AuthScreen( authState = authState, onRetryClick = { viewModel.performAuthentication(biometricAuthenticator) }, ) } } } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/MoneyFlowErrorMapper.kt ================================================ package com.prof18.moneyflow.presentation import com.prof18.moneyflow.domain.entities.MoneyFlowError import com.prof18.moneyflow.domain.entities.MoneyFlowError.AddTransaction import com.prof18.moneyflow.domain.entities.MoneyFlowError.DatabaseExport import com.prof18.moneyflow.domain.entities.MoneyFlowError.DatabaseImport import com.prof18.moneyflow.domain.entities.MoneyFlowError.DatabaseNotFound import com.prof18.moneyflow.domain.entities.MoneyFlowError.DeleteTransaction import com.prof18.moneyflow.domain.entities.MoneyFlowError.GetAllTransaction import com.prof18.moneyflow.domain.entities.MoneyFlowError.GetCategories import com.prof18.moneyflow.domain.entities.MoneyFlowError.GetMoneySummary import com.prof18.moneyflow.presentation.model.UIErrorMessage import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.database_file_not_found import money_flow.shared.generated.resources.error_add_transaction_message import money_flow.shared.generated.resources.error_database_export import money_flow.shared.generated.resources.error_database_import import money_flow.shared.generated.resources.error_delete_transaction_message import money_flow.shared.generated.resources.error_get_all_transaction_message import money_flow.shared.generated.resources.error_get_categories_message import money_flow.shared.generated.resources.error_get_money_summary_message internal class MoneyFlowErrorMapper { fun getUIErrorMessage(error: MoneyFlowError): UIErrorMessage { return UIErrorMessage( message = error.getErrorMessageRes(), ) } private fun MoneyFlowError.getErrorMessageRes() = when (this) { is AddTransaction -> Res.string.error_add_transaction_message is DeleteTransaction -> Res.string.error_delete_transaction_message is GetAllTransaction -> Res.string.error_get_all_transaction_message is GetCategories -> Res.string.error_get_categories_message is GetMoneySummary -> Res.string.error_get_money_summary_message is DatabaseExport -> Res.string.error_database_export is DatabaseImport -> Res.string.error_database_import is DatabaseNotFound -> Res.string.database_file_not_found } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionAction.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction import com.prof18.moneyflow.presentation.model.UIErrorMessage internal sealed class AddTransactionAction { class ShowError(val uiErrorMessage: UIErrorMessage) : AddTransactionAction() object GoBack : AddTransactionAction() } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionScreen.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDefaults import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.presentation.addtransaction.components.IconTextClickableRow import com.prof18.moneyflow.presentation.addtransaction.components.MFTextInput import com.prof18.moneyflow.presentation.addtransaction.components.TransactionTypeTabBar import com.prof18.moneyflow.presentation.categories.data.CategoryUIData import com.prof18.moneyflow.presentation.categories.mapToDrawableResource import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.ui.components.MFTopBar import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.add_transaction_screen import money_flow.shared.generated.resources.cancel import money_flow.shared.generated.resources.confirm import money_flow.shared.generated.resources.description import money_flow.shared.generated.resources.ic_calendar import money_flow.shared.generated.resources.ic_edit import money_flow.shared.generated.resources.ic_money_bill_wave import money_flow.shared.generated.resources.ic_question_circle import money_flow.shared.generated.resources.save import money_flow.shared.generated.resources.select_category import money_flow.shared.generated.resources.today import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import kotlin.time.Clock @Composable @Suppress("LongMethod", "LongParameterList") // TODO: reduce method length @OptIn(ExperimentalMaterial3Api::class) internal fun AddTransactionScreen( categoryState: State, navigateUp: () -> Unit, navigateToCategoryList: () -> Unit, addTransaction: (Long) -> Unit, amountText: String, updateAmountText: (String) -> Unit, descriptionText: String?, updateDescriptionText: (String?) -> Unit, selectedTransactionType: TransactionType, updateTransactionType: (TransactionType) -> Unit, updateSelectedDate: (Long) -> Unit, dateLabel: String?, selectedDateMillis: Long, addTransactionAction: AddTransactionAction?, resetAction: () -> Unit, currencyConfig: CurrencyConfig?, ) { val (showDatePickerDialog, setShowedDatePickerDialog) = remember { mutableStateOf(false) } val datePickerState = rememberDatePickerState(initialSelectedDateMillis = selectedDateMillis) LaunchedEffect(selectedDateMillis) { datePickerState.selectedDateMillis = selectedDateMillis } val snackbarHostState = remember { SnackbarHostState() } addTransactionAction?.let { when (it) { is AddTransactionAction.GoBack -> { navigateUp() resetAction() } is AddTransactionAction.ShowError -> { val messageText = stringResource(it.uiErrorMessage.message) LaunchedEffect(snackbarHostState, resetAction) { snackbarHostState.showSnackbar(messageText) resetAction() } } } } val keyboardController = LocalSoftwareKeyboardController.current val amountLabel = currencyConfig?.let { val decimalPart = if (it.decimalPlaces == 0) { "" } else { ".${"0".repeat(it.decimalPlaces)}" } "${it.symbol} 0$decimalPart" } ?: "€ 0.00" Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { MFTopBar( topAppBarText = stringResource(Res.string.add_transaction_screen), actionTitle = stringResource(Res.string.save), onBackPressed = { navigateUp() }, onActionClicked = { keyboardController?.hide() categoryState.value?.id?.let(addTransaction) }, actionEnabled = categoryState.value?.id != null && amountText.isNotEmpty(), ) }, content = { innerPadding -> Column(modifier = Modifier.padding(innerPadding)) { if (showDatePickerDialog) { DatePickerDialog( onDismissRequest = { setShowedDatePickerDialog(false) }, confirmButton = { TextButton( enabled = datePickerState.selectedDateMillis != null, onClick = { datePickerState.selectedDateMillis?.let { selectedDate -> updateSelectedDate(selectedDate) } setShowedDatePickerDialog(false) }, ) { Text(text = stringResource(Res.string.confirm)) } }, dismissButton = { TextButton(onClick = { setShowedDatePickerDialog(false) }) { Text(text = stringResource(Res.string.cancel)) } }, ) { DatePicker( state = datePickerState, showModeToggle = false, colors = DatePickerDefaults.colors( containerColor = MaterialTheme.colorScheme.surface, ), ) } } TransactionTypeTabBar( transactionType = selectedTransactionType, onTabSelected = { updateTransactionType(it) }, modifier = Modifier .padding(Margins.regular), ) MFTextInput( text = amountText, textStyle = MaterialTheme.typography.bodyLarge, label = amountLabel, leadingIcon = { Icon( painter = painterResource(Res.drawable.ic_money_bill_wave), contentDescription = null, ) }, onTextChange = { updateAmountText(it) }, keyboardType = KeyboardType.Decimal, modifier = Modifier .fillMaxWidth() .padding( start = Margins.regular, end = Margins.regular, top = Margins.small, ), ) MFTextInput( text = descriptionText ?: "", textStyle = MaterialTheme.typography.bodyLarge, label = stringResource(Res.string.description), leadingIcon = { Icon( painter = painterResource(Res.drawable.ic_edit), contentDescription = null, ) }, onTextChange = { updateDescriptionText(it) }, keyboardType = KeyboardType.Text, modifier = Modifier .fillMaxWidth() .padding( start = Margins.regular, end = Margins.regular, top = Margins.regular, ), ) IconTextClickableRow( onClick = { navigateToCategoryList() }, text = categoryState.value?.name ?: stringResource(Res.string.select_category), icon = categoryState.value?.icon?.mapToDrawableResource() ?: Res.drawable.ic_question_circle, isSomethingSelected = categoryState.value?.name != null, modifier = Modifier.padding( start = Margins.regular, end = Margins.regular, top = Margins.medium, ), ) IconTextClickableRow( onClick = { setShowedDatePickerDialog(true) }, text = dateLabel ?: stringResource(Res.string.today), icon = Res.drawable.ic_calendar, modifier = Modifier.padding( start = Margins.regular, end = Margins.regular, top = Margins.medium, bottom = Margins.regular, ), ) } }, ) } @Preview(name = "Add Transaction Screen Light") @Composable private fun AddTransactionScreenPreview() { MoneyFlowTheme { Surface { AddTransactionScreen( categoryState = remember { mutableStateOf( CategoryUIData( id = 1, name = "Food", icon = CategoryIcon.IC_HAMBURGER_SOLID, ), ) }, navigateUp = {}, navigateToCategoryList = {}, addTransaction = {}, amountText = "10.00", updateAmountText = {}, descriptionText = "Pizza 🍕", updateDescriptionText = {}, selectedTransactionType = TransactionType.OUTCOME, updateTransactionType = {}, updateSelectedDate = {}, dateLabel = "11 July 2021", selectedDateMillis = Clock.System.now().toEpochMilliseconds(), addTransactionAction = null, resetAction = {}, currencyConfig = CurrencyConfig( code = "EUR", symbol = "€", decimalPlaces = 2, ), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/TransactionToSave.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction import com.prof18.moneyflow.database.model.TransactionType internal data class TransactionToSave( val dateMillis: Long, val amountCents: Long, val description: String?, val categoryId: Long, val transactionType: TransactionType, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/IconTextClicableRow.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_question_circle import money_flow.shared.generated.resources.icon_content_desc import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable internal fun IconTextClickableRow( onClick: () -> Unit, text: String, icon: DrawableResource, modifier: Modifier = Modifier, isSomethingSelected: Boolean = true, ) { Column( modifier = modifier .border( BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)), RoundedCornerShape(4.dp), ) .clickable(onClick = onClick) .fillMaxWidth() .padding(vertical = 12.dp), ) { Row { Icon( painter = painterResource(icon), contentDescription = "$text ${stringResource(Res.string.icon_content_desc)}", tint = if (isSystemInDarkTheme()) { Color(color = 0xff888a8f) } else { Color(color = 0xff8d989d) }, modifier = Modifier.padding(start = Margins.horizontalIconPadding), ) Spacer(Modifier.width(Margins.textFieldPadding)) @Suppress("MagicNumber") val alpha = if (isSomethingSelected) { 1.0f } else { 0.5f } Text( text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier .alpha(alpha) .align(Alignment.CenterVertically), ) } } } @Preview(name = "IconTextClickableRow Light") @Composable private fun IconTextClickableRowPreview() { MoneyFlowTheme { Surface { IconTextClickableRow( onClick = {}, text = "Select something", icon = Res.drawable.ic_question_circle, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/MFTextField.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction.components import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_edit import org.jetbrains.compose.resources.painterResource // TODO: check padding bottom of the text @Composable internal fun MFTextInput( text: String, label: String?, onTextChange: (String) -> Unit, keyboardType: KeyboardType, textStyle: TextStyle, modifier: Modifier = Modifier, leadingIcon: @Composable (() -> Unit)? = null, ) { val focusManager = LocalFocusManager.current OutlinedTextField( value = text, onValueChange = { onTextChange(it) }, modifier = modifier, textStyle = textStyle, placeholder = { if (label != null) { @Suppress("MagicNumber") Text( text = label, modifier = Modifier.alpha(0.5f), style = textStyle, ) } }, leadingIcon = leadingIcon, keyboardOptions = KeyboardOptions( keyboardType = keyboardType, imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( onDone = { onTextChange(text) focusManager.clearFocus() }, ), ) } @Preview(name = "MFTextInputPreviewWithIcon Light") @Composable private fun MFTextInputPreviewWithIcon() { MoneyFlowTheme { Surface { MFTextInput( text = "This is a text", label = null, onTextChange = { }, keyboardType = KeyboardType.Text, textStyle = MaterialTheme.typography.bodyLarge, leadingIcon = { Icon( painter = painterResource(Res.drawable.ic_edit), contentDescription = null, ) }, ) } } } @Preview(name = "MFTextInputPreview Light") @Composable private fun MFTextInputPreview() { MoneyFlowTheme { Surface { MFTextInput( text = "This is a text", label = null, onTextChange = { }, keyboardType = KeyboardType.Text, textStyle = MaterialTheme.typography.bodyLarge, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/TransactionTypeTabBar.kt ================================================ package com.prof18.moneyflow.presentation.addtransaction.components import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.TabPosition import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.ui.components.ArrowCircleIcon import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.ui.style.downArrowCircleColor import com.prof18.moneyflow.ui.style.downArrowColor import com.prof18.moneyflow.ui.style.upArrowCircleColor import com.prof18.moneyflow.ui.style.upArrowColor import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_arrow_down_rotate import money_flow.shared.generated.resources.ic_arrow_up_rotate import money_flow.shared.generated.resources.transaction_type_income import money_flow.shared.generated.resources.transaction_type_outcome import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.stringResource @Composable internal fun TransactionTypeTabBar( transactionType: TransactionType, onTabSelected: (tabPage: TransactionType) -> Unit, modifier: Modifier = Modifier, ) { TabRow( modifier = modifier, selectedTabIndex = transactionType.ordinal, containerColor = Color.Transparent, indicator = { tabPositions -> TransactionTabIndicator(tabPositions.toImmutableList(), transactionType) }, divider = { }, ) { TransactionTab( boxColor = upArrowCircleColor(), arrowColor = upArrowColor(), iconResource = Res.drawable.ic_arrow_up_rotate, title = stringResource(Res.string.transaction_type_income), onClick = { onTabSelected(TransactionType.INCOME) }, ) TransactionTab( boxColor = downArrowCircleColor(), arrowColor = downArrowColor(), iconResource = Res.drawable.ic_arrow_down_rotate, title = stringResource(Res.string.transaction_type_outcome), onClick = { onTabSelected(TransactionType.OUTCOME) }, ) } } @Composable private fun TransactionTab( boxColor: Color, arrowColor: Color, iconResource: DrawableResource, title: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier .clickable(onClick = onClick) .padding(16.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { ArrowCircleIcon( boxColor = boxColor, iconResource = iconResource, arrowColor = arrowColor, iconSize = 18.dp, ) Spacer(modifier = Modifier.width(16.dp)) Text(text = title) } } @Composable private fun TransactionTabIndicator( tabPositions: ImmutableList, transactionType: TransactionType, ) { val transition = updateTransition(transactionType, label = "tab_selection_transition") val indicatorLeft by transition.animateDp(label = "indicator_left_animation") { page -> tabPositions[page.ordinal].left } val indicatorRight by transition.animateDp(label = "indicator_right_animation") { page -> tabPositions[page.ordinal].right } Box( Modifier .fillMaxSize() .wrapContentSize(align = Alignment.BottomStart) .offset(x = indicatorLeft) .width(indicatorRight - indicatorLeft) .fillMaxSize() .border( BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)), RoundedCornerShape(4.dp), ), ) } @Preview(name = "TransactionTypeTabBarPreview Light") @Composable private fun TransactionTypeTabBarPreview() { MoneyFlowTheme { Surface { TransactionTypeTabBar( transactionType = TransactionType.INCOME, onTabSelected = {}, modifier = Modifier.padding(Margins.small), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/alltransactions/AllTransactionsScreen.kt ================================================ package com.prof18.moneyflow.presentation.alltransactions import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.domain.entities.TransactionTypeUI import com.prof18.moneyflow.features.alltransactions.AllTransactionsUiState import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.ui.components.ErrorView import com.prof18.moneyflow.ui.components.Loader import com.prof18.moneyflow.ui.components.MFTopBar import com.prof18.moneyflow.ui.components.TransactionCard import com.prof18.moneyflow.ui.style.MoneyFlowTheme import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.all_transactions import org.jetbrains.compose.resources.stringResource @Composable internal fun AllTransactionsScreen( stateFlow: StateFlow, loadNextPage: () -> Unit, navigateUp: () -> Unit = {}, ) { Scaffold( topBar = { MFTopBar( topAppBarText = stringResource(Res.string.all_transactions), onBackPressed = { navigateUp() }, ) }, content = { innerPadding -> val uiState = stateFlow.collectAsState().value val currencyConfig = uiState.currencyConfig LazyColumn(modifier = Modifier.padding(innerPadding)) { when { uiState.error != null -> { item { ErrorView(uiErrorMessage = uiState.error) } } uiState.isLoading -> item { Loader() } else -> { // TODO: create some sort of sticky header by grouping by date items( count = uiState.transactions.size, ) { index -> val transaction = uiState.transactions[index] TransactionCard( transaction = transaction, onLongPress = { /*TODO: add long press on transaction*/ }, onClick = { /*TODO: add click on transaction*/ }, hideSensitiveData = false, // TODO: Hide sensitive data on transaction card currencyConfig = currencyConfig, ) HorizontalDivider() if ( index == uiState.transactions.lastIndex && !uiState.endReached && !uiState.isLoadingMore ) { loadNextPage() } } if (uiState.isLoadingMore) { item { Loader() } } } } } }, ) } @Preview(name = "AllTransactionsScreenPreviews Light") @Composable private fun AllTransactionsScreenPreviews() { MoneyFlowTheme { AllTransactionsScreen( stateFlow = MutableStateFlow( AllTransactionsUiState( transactions = listOf( SampleTransactions.iceCream, SampleTransactions.tip, ), currencyConfig = CurrencyConfig("EUR", "€", 2), ), ), loadNextPage = {}, navigateUp = {}, ) } } private object SampleTransactions { val iceCream = MoneyTransaction( id = 0, title = "Ice Cream", icon = CategoryIcon.IC_ICE_CREAM_SOLID, amountCents = 1_000, type = TransactionTypeUI.EXPENSE, milliseconds = 0, formattedDate = "12 July 2021", ) val tip = MoneyTransaction( id = 1, title = "Tip", icon = CategoryIcon.IC_MONEY_CHECK_ALT_SOLID, amountCents = 5_000, type = TransactionTypeUI.INCOME, milliseconds = 0, formattedDate = "12 July 2021", ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/auth/AuthInProgressScreen.kt ================================================ package com.prof18.moneyflow.presentation.auth import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.auth_error import money_flow.shared.generated.resources.auth_failed import money_flow.shared.generated.resources.authenticating import money_flow.shared.generated.resources.retry import org.jetbrains.compose.resources.stringResource @Composable internal fun AuthScreen( authState: AuthState, onRetryClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { if (authState == AuthState.AUTH_IN_PROGRESS) { CircularProgressIndicator() Spacer(modifier = Modifier.size(Margins.regular)) } Text( text = authState.getAuthMessage(), style = MaterialTheme.typography.bodyLarge, ) if (authState == AuthState.AUTH_ERROR) { Spacer(modifier = Modifier.size(Margins.regular)) Button(onClick = { onRetryClick() }) { Text( text = stringResource(Res.string.retry), style = MaterialTheme.typography.labelLarge, ) } } } } @Composable private fun AuthState.getAuthMessage(): String { return when (this) { AuthState.AUTHENTICATED -> "" AuthState.NOT_AUTHENTICATED -> stringResource(Res.string.auth_failed) AuthState.AUTH_IN_PROGRESS -> stringResource(Res.string.authenticating) AuthState.AUTH_ERROR -> stringResource(Res.string.auth_error) } } @Preview(name = "AuthScreenProgress Light") @Composable private fun AuthScreenProgressPreview() { MoneyFlowTheme { Surface { AuthScreen( authState = AuthState.AUTH_IN_PROGRESS, onRetryClick = {}, ) } } } @Preview(name = "uthScreenFail Light") @Composable private fun AuthScreenFailPreview() { MoneyFlowTheme { Surface { AuthScreen( authState = AuthState.NOT_AUTHENTICATED, onRetryClick = {}, ) } } } @Preview(name = "AuthScreenError Light") @Composable private fun AuthScreenErrorPreview() { MoneyFlowTheme { Surface { AuthScreen( authState = AuthState.AUTH_ERROR, onRetryClick = {}, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/auth/AuthState.kt ================================================ package com.prof18.moneyflow.presentation.auth internal enum class AuthState { AUTHENTICATED, NOT_AUTHENTICATED, AUTH_IN_PROGRESS, AUTH_ERROR, } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/budget/BudgetScreen.kt ================================================ package com.prof18.moneyflow.presentation.budget import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @Composable internal fun BudgetScreen() { Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { Text("Coming Soon") } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/categories/CategoriesScreen.kt ================================================ package com.prof18.moneyflow.presentation.categories import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.presentation.categories.components.CategoryCard import com.prof18.moneyflow.presentation.categories.data.CategoryUIData import com.prof18.moneyflow.presentation.categories.data.toCategoryUIData import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.presentation.model.UIErrorMessage import com.prof18.moneyflow.ui.components.ErrorView import com.prof18.moneyflow.ui.components.Loader import com.prof18.moneyflow.ui.components.MFTopBar import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.add import money_flow.shared.generated.resources.categories_screen import money_flow.shared.generated.resources.error_get_categories_message import org.jetbrains.compose.resources.stringResource @Composable internal fun CategoriesScreen( navigateUp: () -> Unit, sendCategoryBack: (CategoryUIData) -> Unit, isFromAddTransaction: Boolean, categoryModel: CategoryModel, ) { Scaffold( topBar = { MFTopBar( topAppBarText = stringResource(Res.string.categories_screen), actionTitle = stringResource(Res.string.add), onBackPressed = { navigateUp() }, onActionClicked = { // TODO: open a new screen to add a new category }, actionEnabled = true, ) }, content = { innerPadding -> when (categoryModel) { CategoryModel.Loading -> Loader() is CategoryModel.Error -> { ErrorView(uiErrorMessage = categoryModel.uiErrorMessage) } is CategoryModel.CategoryState -> { LazyColumn(modifier = Modifier.padding(innerPadding)) { items(categoryModel.categories) { CategoryCard( category = it, onClick = { category -> if (isFromAddTransaction) { sendCategoryBack(category.toCategoryUIData()) } else { navigateUp() } }, ) HorizontalDivider() } } } } }, ) } @Preview(name = "CategoriesScreen Light") @Composable private fun CategoriesScreenPreview() { MoneyFlowTheme { Surface { CategoriesScreen( navigateUp = { }, sendCategoryBack = { }, isFromAddTransaction = true, categoryModel = CategoryModel.CategoryState( categories = listOf( Category( id = 0, name = "Food", icon = CategoryIcon.IC_HAMBURGER_SOLID, type = TransactionType.OUTCOME, createdAtMillis = 1, ), Category( id = 0, name = "Drinks", icon = CategoryIcon.IC_COCKTAIL_SOLID, type = TransactionType.OUTCOME, createdAtMillis = 1, ), ), ), ) } } } @Preview(name = "CategoriesScreenError Light") @Composable private fun CategoriesScreenErrorPreview() { MoneyFlowTheme { Surface { CategoriesScreen( navigateUp = { }, sendCategoryBack = { }, isFromAddTransaction = true, categoryModel = CategoryModel.Error( uiErrorMessage = UIErrorMessage( message = Res.string.error_get_categories_message, ), ), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/categories/CategoryModel.kt ================================================ package com.prof18.moneyflow.presentation.categories import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.presentation.model.UIErrorMessage internal sealed class CategoryModel { object Loading : CategoryModel() data class Error(val uiErrorMessage: UIErrorMessage) : CategoryModel() data class CategoryState(val categories: List) : CategoryModel() } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/categories/IconCategoryMapper.kt ================================================ package com.prof18.moneyflow.presentation.categories import com.prof18.moneyflow.presentation.model.CategoryIcon import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_address_book import money_flow.shared.generated.resources.ic_address_card import money_flow.shared.generated.resources.ic_adjust_solid import money_flow.shared.generated.resources.ic_air_freshener_solid import money_flow.shared.generated.resources.ic_algolia import money_flow.shared.generated.resources.ic_allergies_solid import money_flow.shared.generated.resources.ic_ambulance_solid import money_flow.shared.generated.resources.ic_anchor_solid import money_flow.shared.generated.resources.ic_android import money_flow.shared.generated.resources.ic_angle_down_solid import money_flow.shared.generated.resources.ic_angle_left_solid import money_flow.shared.generated.resources.ic_angle_right_solid import money_flow.shared.generated.resources.ic_angle_up_solid import money_flow.shared.generated.resources.ic_apple import money_flow.shared.generated.resources.ic_apple_alt_solid import money_flow.shared.generated.resources.ic_archive_solid import money_flow.shared.generated.resources.ic_archway_solid import money_flow.shared.generated.resources.ic_arrow_down_solid import money_flow.shared.generated.resources.ic_arrow_left_solid import money_flow.shared.generated.resources.ic_arrow_right_solid import money_flow.shared.generated.resources.ic_arrow_up_solid import money_flow.shared.generated.resources.ic_asterisk_solid import money_flow.shared.generated.resources.ic_at_solid import money_flow.shared.generated.resources.ic_atlas_solid import money_flow.shared.generated.resources.ic_atom_solid import money_flow.shared.generated.resources.ic_award_solid import money_flow.shared.generated.resources.ic_baby_carriage_solid import money_flow.shared.generated.resources.ic_bacon_solid import money_flow.shared.generated.resources.ic_balance_scale_left_solid import money_flow.shared.generated.resources.ic_band_aid_solid import money_flow.shared.generated.resources.ic_baseball_ball_solid import money_flow.shared.generated.resources.ic_basketball_ball_solid import money_flow.shared.generated.resources.ic_bath_solid import money_flow.shared.generated.resources.ic_battery_three_quarters_solid import money_flow.shared.generated.resources.ic_bed_solid import money_flow.shared.generated.resources.ic_beer_solid import money_flow.shared.generated.resources.ic_bell import money_flow.shared.generated.resources.ic_bell_slash import money_flow.shared.generated.resources.ic_bicycle_solid import money_flow.shared.generated.resources.ic_biking_solid import money_flow.shared.generated.resources.ic_binoculars_solid import money_flow.shared.generated.resources.ic_birthday_cake_solid import money_flow.shared.generated.resources.ic_bitcoin import money_flow.shared.generated.resources.ic_black_tie import money_flow.shared.generated.resources.ic_blender_solid import money_flow.shared.generated.resources.ic_blind_solid import money_flow.shared.generated.resources.ic_bolt_solid import money_flow.shared.generated.resources.ic_bomb_solid import money_flow.shared.generated.resources.ic_bone_solid import money_flow.shared.generated.resources.ic_bong_solid import money_flow.shared.generated.resources.ic_book_open_solid import money_flow.shared.generated.resources.ic_book_solid import money_flow.shared.generated.resources.ic_bookmark import money_flow.shared.generated.resources.ic_bowling_ball_solid import money_flow.shared.generated.resources.ic_box_solid import money_flow.shared.generated.resources.ic_brain_solid import money_flow.shared.generated.resources.ic_bread_slice_solid import money_flow.shared.generated.resources.ic_briefcase_medical_solid import money_flow.shared.generated.resources.ic_briefcase_solid import money_flow.shared.generated.resources.ic_broadcast_tower_solid import money_flow.shared.generated.resources.ic_broom_solid import money_flow.shared.generated.resources.ic_brush_solid import money_flow.shared.generated.resources.ic_bug_solid import money_flow.shared.generated.resources.ic_building import money_flow.shared.generated.resources.ic_bullhorn_solid import money_flow.shared.generated.resources.ic_bullseye_solid import money_flow.shared.generated.resources.ic_burn_solid import money_flow.shared.generated.resources.ic_bus_solid import money_flow.shared.generated.resources.ic_calculator_solid import money_flow.shared.generated.resources.ic_calendar import money_flow.shared.generated.resources.ic_camera_solid import money_flow.shared.generated.resources.ic_campground_solid import money_flow.shared.generated.resources.ic_candy_cane_solid import money_flow.shared.generated.resources.ic_capsules_solid import money_flow.shared.generated.resources.ic_car_alt_solid import money_flow.shared.generated.resources.ic_car_side_solid import money_flow.shared.generated.resources.ic_caret_down_solid import money_flow.shared.generated.resources.ic_caret_left_solid import money_flow.shared.generated.resources.ic_caret_right_solid import money_flow.shared.generated.resources.ic_caret_up_solid import money_flow.shared.generated.resources.ic_carrot_solid import money_flow.shared.generated.resources.ic_cart_arrow_down_solid import money_flow.shared.generated.resources.ic_cash_register_solid import money_flow.shared.generated.resources.ic_cat_solid import money_flow.shared.generated.resources.ic_certificate_solid import money_flow.shared.generated.resources.ic_chair_solid import money_flow.shared.generated.resources.ic_chalkboard_solid import money_flow.shared.generated.resources.ic_chalkboard_teacher_solid import money_flow.shared.generated.resources.ic_charging_station_solid import money_flow.shared.generated.resources.ic_chart_area_solid import money_flow.shared.generated.resources.ic_chart_bar import money_flow.shared.generated.resources.ic_chart_line_solid import money_flow.shared.generated.resources.ic_chart_pie_solid import money_flow.shared.generated.resources.ic_check_circle import money_flow.shared.generated.resources.ic_cheese_solid import money_flow.shared.generated.resources.ic_church_solid import money_flow.shared.generated.resources.ic_city_solid import money_flow.shared.generated.resources.ic_clinic_medical_solid import money_flow.shared.generated.resources.ic_clipboard import money_flow.shared.generated.resources.ic_clock import money_flow.shared.generated.resources.ic_cloud_download_alt_solid import money_flow.shared.generated.resources.ic_cloud_solid import money_flow.shared.generated.resources.ic_cloud_upload_alt_solid import money_flow.shared.generated.resources.ic_cocktail_solid import money_flow.shared.generated.resources.ic_code_branch_solid import money_flow.shared.generated.resources.ic_code_solid import money_flow.shared.generated.resources.ic_coffee_solid import money_flow.shared.generated.resources.ic_cog_solid import money_flow.shared.generated.resources.ic_coins_solid import money_flow.shared.generated.resources.ic_comment_alt import money_flow.shared.generated.resources.ic_compact_disc_solid import money_flow.shared.generated.resources.ic_compass import money_flow.shared.generated.resources.ic_concierge_bell_solid import money_flow.shared.generated.resources.ic_cookie_bite_solid import money_flow.shared.generated.resources.ic_couch_solid import money_flow.shared.generated.resources.ic_credit_card import money_flow.shared.generated.resources.ic_crown_solid import money_flow.shared.generated.resources.ic_cubes_solid import money_flow.shared.generated.resources.ic_cut_solid import money_flow.shared.generated.resources.ic_desktop_solid import money_flow.shared.generated.resources.ic_diaspora import money_flow.shared.generated.resources.ic_dice_d6_solid import money_flow.shared.generated.resources.ic_dna_solid import money_flow.shared.generated.resources.ic_dog_solid import money_flow.shared.generated.resources.ic_dollar_sign_solid import money_flow.shared.generated.resources.ic_dolly_flatbed_solid import money_flow.shared.generated.resources.ic_dolly_solid import money_flow.shared.generated.resources.ic_donate_solid import money_flow.shared.generated.resources.ic_drafting_compass_solid import money_flow.shared.generated.resources.ic_drum_solid import money_flow.shared.generated.resources.ic_drumstick_bite_solid import money_flow.shared.generated.resources.ic_dumbbell_solid import money_flow.shared.generated.resources.ic_dumpster_solid import money_flow.shared.generated.resources.ic_edit import money_flow.shared.generated.resources.ic_egg_solid import money_flow.shared.generated.resources.ic_envelope import money_flow.shared.generated.resources.ic_envelope_open import money_flow.shared.generated.resources.ic_eraser_solid import money_flow.shared.generated.resources.ic_euro_sign_solid import money_flow.shared.generated.resources.ic_exchange_alt_solid import money_flow.shared.generated.resources.ic_exclamation_circle_solid import money_flow.shared.generated.resources.ic_exclamation_triangle_solid import money_flow.shared.generated.resources.ic_expeditedssl import money_flow.shared.generated.resources.ic_external_link_alt_solid import money_flow.shared.generated.resources.ic_eye_dropper_solid import money_flow.shared.generated.resources.ic_fan_solid import money_flow.shared.generated.resources.ic_fax_solid import money_flow.shared.generated.resources.ic_feather_alt_solid import money_flow.shared.generated.resources.ic_female_solid import money_flow.shared.generated.resources.ic_fighter_jet_solid import money_flow.shared.generated.resources.ic_file import money_flow.shared.generated.resources.ic_file_alt import money_flow.shared.generated.resources.ic_file_audio import money_flow.shared.generated.resources.ic_file_code import money_flow.shared.generated.resources.ic_file_csv_solid import money_flow.shared.generated.resources.ic_file_export_solid import money_flow.shared.generated.resources.ic_file_import_solid import money_flow.shared.generated.resources.ic_file_invoice_dollar_solid import money_flow.shared.generated.resources.ic_file_invoice_solid import money_flow.shared.generated.resources.ic_file_pdf import money_flow.shared.generated.resources.ic_fill_solid import money_flow.shared.generated.resources.ic_film_solid import money_flow.shared.generated.resources.ic_fire_alt_solid import money_flow.shared.generated.resources.ic_fire_extinguisher_solid import money_flow.shared.generated.resources.ic_first_aid_solid import money_flow.shared.generated.resources.ic_fish_solid import money_flow.shared.generated.resources.ic_flag import money_flow.shared.generated.resources.ic_flag_checkered_solid import money_flow.shared.generated.resources.ic_flask_solid import money_flow.shared.generated.resources.ic_fly import money_flow.shared.generated.resources.ic_folder import money_flow.shared.generated.resources.ic_football_ball_solid import money_flow.shared.generated.resources.ic_fort_awesome import money_flow.shared.generated.resources.ic_frown import money_flow.shared.generated.resources.ic_futbol import money_flow.shared.generated.resources.ic_gamepad_solid import money_flow.shared.generated.resources.ic_gas_pump_solid import money_flow.shared.generated.resources.ic_gavel_solid import money_flow.shared.generated.resources.ic_gift_solid import money_flow.shared.generated.resources.ic_glass_cheers_solid import money_flow.shared.generated.resources.ic_glass_martini_alt_solid import money_flow.shared.generated.resources.ic_globe_solid import money_flow.shared.generated.resources.ic_golf_ball_solid import money_flow.shared.generated.resources.ic_gopuram_solid import money_flow.shared.generated.resources.ic_graduation_cap_solid import money_flow.shared.generated.resources.ic_guitar_solid import money_flow.shared.generated.resources.ic_hamburger_solid import money_flow.shared.generated.resources.ic_hammer_solid import money_flow.shared.generated.resources.ic_hat_cowboy_solid import money_flow.shared.generated.resources.ic_hdd import money_flow.shared.generated.resources.ic_headphones_solid import money_flow.shared.generated.resources.ic_helicopter_solid import money_flow.shared.generated.resources.ic_highlighter_solid import money_flow.shared.generated.resources.ic_hiking_solid import money_flow.shared.generated.resources.ic_home_solid import money_flow.shared.generated.resources.ic_horse_head_solid import money_flow.shared.generated.resources.ic_hospital import money_flow.shared.generated.resources.ic_hotdog_solid import money_flow.shared.generated.resources.ic_hourglass_half_solid import money_flow.shared.generated.resources.ic_ice_cream_solid import money_flow.shared.generated.resources.ic_id_card import money_flow.shared.generated.resources.ic_image import money_flow.shared.generated.resources.ic_inbox_solid import money_flow.shared.generated.resources.ic_industry_solid import money_flow.shared.generated.resources.ic_itunes_note import money_flow.shared.generated.resources.ic_key_solid import money_flow.shared.generated.resources.ic_keyboard import money_flow.shared.generated.resources.ic_landmark_solid import money_flow.shared.generated.resources.ic_laptop_solid import money_flow.shared.generated.resources.ic_lightbulb import money_flow.shared.generated.resources.ic_list_ul_solid import money_flow.shared.generated.resources.ic_luggage_cart_solid import money_flow.shared.generated.resources.ic_mail_bulk_solid import money_flow.shared.generated.resources.ic_male_solid import money_flow.shared.generated.resources.ic_map_marked_alt_solid import money_flow.shared.generated.resources.ic_marker_solid import money_flow.shared.generated.resources.ic_mars_solid import money_flow.shared.generated.resources.ic_mask_solid import money_flow.shared.generated.resources.ic_medal_solid import money_flow.shared.generated.resources.ic_medapps import money_flow.shared.generated.resources.ic_medkit_solid import money_flow.shared.generated.resources.ic_mercury_solid import money_flow.shared.generated.resources.ic_microchip_solid import money_flow.shared.generated.resources.ic_microphone_alt_solid import money_flow.shared.generated.resources.ic_microscope_solid import money_flow.shared.generated.resources.ic_mobile_solid import money_flow.shared.generated.resources.ic_money_check_alt_solid import money_flow.shared.generated.resources.ic_mortar_pestle_solid import money_flow.shared.generated.resources.ic_motorcycle_solid import money_flow.shared.generated.resources.ic_mountain_solid import money_flow.shared.generated.resources.ic_mug_hot_solid import money_flow.shared.generated.resources.ic_oil_can_solid import money_flow.shared.generated.resources.ic_pager_solid import money_flow.shared.generated.resources.ic_paint_roller_solid import money_flow.shared.generated.resources.ic_paperclip_solid import money_flow.shared.generated.resources.ic_parachute_box_solid import money_flow.shared.generated.resources.ic_parking_solid import money_flow.shared.generated.resources.ic_passport_solid import money_flow.shared.generated.resources.ic_paw_solid import money_flow.shared.generated.resources.ic_pen_alt_solid import money_flow.shared.generated.resources.ic_pen_solid import money_flow.shared.generated.resources.ic_phone_solid import money_flow.shared.generated.resources.ic_photo_video_solid import money_flow.shared.generated.resources.ic_piggy_bank_solid import money_flow.shared.generated.resources.ic_pills_solid import money_flow.shared.generated.resources.ic_pizza_slice_solid import money_flow.shared.generated.resources.ic_plane_solid import money_flow.shared.generated.resources.ic_plug_solid import money_flow.shared.generated.resources.ic_pound_sign_solid import money_flow.shared.generated.resources.ic_prescription_bottle_solid import money_flow.shared.generated.resources.ic_print_solid import money_flow.shared.generated.resources.ic_question_circle import money_flow.shared.generated.resources.ic_readme import money_flow.shared.generated.resources.ic_recycle_solid import money_flow.shared.generated.resources.ic_restroom_solid import money_flow.shared.generated.resources.ic_road_solid import money_flow.shared.generated.resources.ic_robot_solid import money_flow.shared.generated.resources.ic_rocket_solid import money_flow.shared.generated.resources.ic_running_solid import money_flow.shared.generated.resources.ic_screwdriver_solid import money_flow.shared.generated.resources.ic_scroll_solid import money_flow.shared.generated.resources.ic_seedling_solid import money_flow.shared.generated.resources.ic_server_solid import money_flow.shared.generated.resources.ic_shield_alt_solid import money_flow.shared.generated.resources.ic_ship_solid import money_flow.shared.generated.resources.ic_shipping_fast_solid import money_flow.shared.generated.resources.ic_shopping_bag_solid import money_flow.shared.generated.resources.ic_shopping_cart_solid import money_flow.shared.generated.resources.ic_shuttle_van_solid import money_flow.shared.generated.resources.ic_signal_solid import money_flow.shared.generated.resources.ic_sim_card_solid import money_flow.shared.generated.resources.ic_skating_solid import money_flow.shared.generated.resources.ic_skiing_nordic_solid import money_flow.shared.generated.resources.ic_skiing_solid import money_flow.shared.generated.resources.ic_smoking_solid import money_flow.shared.generated.resources.ic_sms_solid import money_flow.shared.generated.resources.ic_snowboarding_solid import money_flow.shared.generated.resources.ic_snowflake import money_flow.shared.generated.resources.ic_socks_solid import money_flow.shared.generated.resources.ic_spider_solid import money_flow.shared.generated.resources.ic_spray_can_solid import money_flow.shared.generated.resources.ic_stamp_solid import money_flow.shared.generated.resources.ic_star_of_life_solid import money_flow.shared.generated.resources.ic_stethoscope_solid import money_flow.shared.generated.resources.ic_sticky_note import money_flow.shared.generated.resources.ic_stopwatch_solid import money_flow.shared.generated.resources.ic_store_alt_solid import money_flow.shared.generated.resources.ic_subway_solid import money_flow.shared.generated.resources.ic_suitcase_solid import money_flow.shared.generated.resources.ic_swimmer_solid import money_flow.shared.generated.resources.ic_syringe_solid import money_flow.shared.generated.resources.ic_table_tennis_solid import money_flow.shared.generated.resources.ic_tablet_solid import money_flow.shared.generated.resources.ic_tachometer_alt_solid import money_flow.shared.generated.resources.ic_tag_solid import money_flow.shared.generated.resources.ic_taxi_solid import money_flow.shared.generated.resources.ic_temperature_high_solid import money_flow.shared.generated.resources.ic_terminal_solid import money_flow.shared.generated.resources.ic_theater_masks_solid import money_flow.shared.generated.resources.ic_thermometer_full_solid import money_flow.shared.generated.resources.ic_ticket_alt_solid import money_flow.shared.generated.resources.ic_tint_solid import money_flow.shared.generated.resources.ic_toilet_paper_solid import money_flow.shared.generated.resources.ic_toolbox_solid import money_flow.shared.generated.resources.ic_tools_solid import money_flow.shared.generated.resources.ic_tooth_solid import money_flow.shared.generated.resources.ic_tractor_solid import money_flow.shared.generated.resources.ic_train_solid import money_flow.shared.generated.resources.ic_trash_alt import money_flow.shared.generated.resources.ic_tree_solid import money_flow.shared.generated.resources.ic_trophy_solid import money_flow.shared.generated.resources.ic_truck_loading_solid import money_flow.shared.generated.resources.ic_truck_moving_solid import money_flow.shared.generated.resources.ic_truck_pickup_solid import money_flow.shared.generated.resources.ic_tshirt_solid import money_flow.shared.generated.resources.ic_tv_solid import money_flow.shared.generated.resources.ic_university_solid import money_flow.shared.generated.resources.ic_user import money_flow.shared.generated.resources.ic_user_friends_solid import money_flow.shared.generated.resources.ic_utensils_solid import money_flow.shared.generated.resources.ic_venus_solid import money_flow.shared.generated.resources.ic_vial_solid import money_flow.shared.generated.resources.ic_video_solid import money_flow.shared.generated.resources.ic_volleyball_ball_solid import money_flow.shared.generated.resources.ic_volume_up_solid import money_flow.shared.generated.resources.ic_walking_solid import money_flow.shared.generated.resources.ic_wallet_solid import money_flow.shared.generated.resources.ic_wine_glass_solid import money_flow.shared.generated.resources.ic_wrench_solid import money_flow.shared.generated.resources.ic_yen_sign_solid import org.jetbrains.compose.resources.DrawableResource @Suppress("LongMethod", "ComplexMethod") internal fun CategoryIcon.mapToDrawableResource(): DrawableResource { return when (this) { CategoryIcon.IC_ADDRESS_BOOK -> Res.drawable.ic_address_book CategoryIcon.IC_ADDRESS_CARD -> Res.drawable.ic_address_card CategoryIcon.IC_ADJUST_SOLID -> Res.drawable.ic_adjust_solid CategoryIcon.IC_AIR_FRESHENER_SOLID -> Res.drawable.ic_air_freshener_solid CategoryIcon.IC_ALGOLIA -> Res.drawable.ic_algolia CategoryIcon.IC_ALLERGIES_SOLID -> Res.drawable.ic_allergies_solid CategoryIcon.IC_AMBULANCE_SOLID -> Res.drawable.ic_ambulance_solid CategoryIcon.IC_ANCHOR_SOLID -> Res.drawable.ic_anchor_solid CategoryIcon.IC_ANDROID -> Res.drawable.ic_android CategoryIcon.IC_ANGLE_DOWN_SOLID -> Res.drawable.ic_angle_down_solid CategoryIcon.IC_ANGLE_LEFT_SOLID -> Res.drawable.ic_angle_left_solid CategoryIcon.IC_ANGLE_RIGHT_SOLID -> Res.drawable.ic_angle_right_solid CategoryIcon.IC_ANGLE_UP_SOLID -> Res.drawable.ic_angle_up_solid CategoryIcon.IC_APPLE -> Res.drawable.ic_apple CategoryIcon.IC_APPLE_ALT_SOLID -> Res.drawable.ic_apple_alt_solid CategoryIcon.IC_ARCHIVE_SOLID -> Res.drawable.ic_archive_solid CategoryIcon.IC_ARCHWAY_SOLID -> Res.drawable.ic_archway_solid CategoryIcon.IC_ARROW_DOWN_SOLID -> Res.drawable.ic_arrow_down_solid CategoryIcon.IC_ARROW_LEFT_SOLID -> Res.drawable.ic_arrow_left_solid CategoryIcon.IC_ARROW_RIGHT_SOLID -> Res.drawable.ic_arrow_right_solid CategoryIcon.IC_ARROW_UP_SOLID -> Res.drawable.ic_arrow_up_solid CategoryIcon.IC_ASTERISK_SOLID -> Res.drawable.ic_asterisk_solid CategoryIcon.IC_AT_SOLID -> Res.drawable.ic_at_solid CategoryIcon.IC_ATLAS_SOLID -> Res.drawable.ic_atlas_solid CategoryIcon.IC_ATOM_SOLID -> Res.drawable.ic_atom_solid CategoryIcon.IC_AWARD_SOLID -> Res.drawable.ic_award_solid CategoryIcon.IC_BABY_CARRIAGE_SOLID -> Res.drawable.ic_baby_carriage_solid CategoryIcon.IC_BACON_SOLID -> Res.drawable.ic_bacon_solid CategoryIcon.IC_BALANCE_SCALE_LEFT_SOLID -> Res.drawable.ic_balance_scale_left_solid CategoryIcon.IC_BAND_AID_SOLID -> Res.drawable.ic_band_aid_solid CategoryIcon.IC_BASEBALL_BALL_SOLID -> Res.drawable.ic_baseball_ball_solid CategoryIcon.IC_BASKETBALL_BALL_SOLID -> Res.drawable.ic_basketball_ball_solid CategoryIcon.IC_BATH_SOLID -> Res.drawable.ic_bath_solid CategoryIcon.IC_BATTERY_THREE_QUARTERS_SOLID -> Res.drawable.ic_battery_three_quarters_solid CategoryIcon.IC_BED_SOLID -> Res.drawable.ic_bed_solid CategoryIcon.IC_BEER_SOLID -> Res.drawable.ic_beer_solid CategoryIcon.IC_BELL -> Res.drawable.ic_bell CategoryIcon.IC_BELL_SLASH -> Res.drawable.ic_bell_slash CategoryIcon.IC_BICYCLE_SOLID -> Res.drawable.ic_bicycle_solid CategoryIcon.IC_BIKING_SOLID -> Res.drawable.ic_biking_solid CategoryIcon.IC_BINOCULARS_SOLID -> Res.drawable.ic_binoculars_solid CategoryIcon.IC_BIRTHDAY_CAKE_SOLID -> Res.drawable.ic_birthday_cake_solid CategoryIcon.IC_BITCOIN -> Res.drawable.ic_bitcoin CategoryIcon.IC_BLACK_TIE -> Res.drawable.ic_black_tie CategoryIcon.IC_BLENDER_SOLID -> Res.drawable.ic_blender_solid CategoryIcon.IC_BLIND_SOLID -> Res.drawable.ic_blind_solid CategoryIcon.IC_BOLT_SOLID -> Res.drawable.ic_bolt_solid CategoryIcon.IC_BOMB_SOLID -> Res.drawable.ic_bomb_solid CategoryIcon.IC_BONE_SOLID -> Res.drawable.ic_bone_solid CategoryIcon.IC_BONG_SOLID -> Res.drawable.ic_bong_solid CategoryIcon.IC_BOOK_OPEN_SOLID -> Res.drawable.ic_book_open_solid CategoryIcon.IC_BOOK_SOLID -> Res.drawable.ic_book_solid CategoryIcon.IC_BOOKMARK -> Res.drawable.ic_bookmark CategoryIcon.IC_BOWLING_BALL_SOLID -> Res.drawable.ic_bowling_ball_solid CategoryIcon.IC_BOX_SOLID -> Res.drawable.ic_box_solid CategoryIcon.IC_BRAIN_SOLID -> Res.drawable.ic_brain_solid CategoryIcon.IC_BREAD_SLICE_SOLID -> Res.drawable.ic_bread_slice_solid CategoryIcon.IC_BRIEFCASE_MEDICAL_SOLID -> Res.drawable.ic_briefcase_medical_solid CategoryIcon.IC_BRIEFCASE_SOLID -> Res.drawable.ic_briefcase_solid CategoryIcon.IC_BROADCAST_TOWER_SOLID -> Res.drawable.ic_broadcast_tower_solid CategoryIcon.IC_BROOM_SOLID -> Res.drawable.ic_broom_solid CategoryIcon.IC_BRUSH_SOLID -> Res.drawable.ic_brush_solid CategoryIcon.IC_BUG_SOLID -> Res.drawable.ic_bug_solid CategoryIcon.IC_BUILDING -> Res.drawable.ic_building CategoryIcon.IC_BULLHORN_SOLID -> Res.drawable.ic_bullhorn_solid CategoryIcon.IC_BULLSEYE_SOLID -> Res.drawable.ic_bullseye_solid CategoryIcon.IC_BURN_SOLID -> Res.drawable.ic_burn_solid CategoryIcon.IC_BUS_SOLID -> Res.drawable.ic_bus_solid CategoryIcon.IC_CALCULATOR_SOLID -> Res.drawable.ic_calculator_solid CategoryIcon.IC_CALENDAR -> Res.drawable.ic_calendar CategoryIcon.IC_CAMERA_SOLID -> Res.drawable.ic_camera_solid CategoryIcon.IC_CAMPGROUND_SOLID -> Res.drawable.ic_campground_solid CategoryIcon.IC_CANDY_CANE_SOLID -> Res.drawable.ic_candy_cane_solid CategoryIcon.IC_CAPSULES_SOLID -> Res.drawable.ic_capsules_solid CategoryIcon.IC_CAR_ALT_SOLID -> Res.drawable.ic_car_alt_solid CategoryIcon.IC_CAR_SIDE_SOLID -> Res.drawable.ic_car_side_solid CategoryIcon.IC_CARET_DOWN_SOLID -> Res.drawable.ic_caret_down_solid CategoryIcon.IC_CARET_LEFT_SOLID -> Res.drawable.ic_caret_left_solid CategoryIcon.IC_CARET_RIGHT_SOLID -> Res.drawable.ic_caret_right_solid CategoryIcon.IC_CARET_UP_SOLID -> Res.drawable.ic_caret_up_solid CategoryIcon.IC_CARROT_SOLID -> Res.drawable.ic_carrot_solid CategoryIcon.IC_CART_ARROW_DOWN_SOLID -> Res.drawable.ic_cart_arrow_down_solid CategoryIcon.IC_CASH_REGISTER_SOLID -> Res.drawable.ic_cash_register_solid CategoryIcon.IC_CAT_SOLID -> Res.drawable.ic_cat_solid CategoryIcon.IC_CERTIFICATE_SOLID -> Res.drawable.ic_certificate_solid CategoryIcon.IC_CHAIR_SOLID -> Res.drawable.ic_chair_solid CategoryIcon.IC_CHALKBOARD_SOLID -> Res.drawable.ic_chalkboard_solid CategoryIcon.IC_CHALKBOARD_TEACHER_SOLID -> Res.drawable.ic_chalkboard_teacher_solid CategoryIcon.IC_CHARGING_STATION_SOLID -> Res.drawable.ic_charging_station_solid CategoryIcon.IC_CHART_AREA_SOLID -> Res.drawable.ic_chart_area_solid CategoryIcon.IC_CHART_BAR -> Res.drawable.ic_chart_bar CategoryIcon.IC_CHART_LINE_SOLID -> Res.drawable.ic_chart_line_solid CategoryIcon.IC_CHART_PIE_SOLID -> Res.drawable.ic_chart_pie_solid CategoryIcon.IC_CHECK_CIRCLE -> Res.drawable.ic_check_circle CategoryIcon.IC_CHEESE_SOLID -> Res.drawable.ic_cheese_solid CategoryIcon.IC_CHURCH_SOLID -> Res.drawable.ic_church_solid CategoryIcon.IC_CITY_SOLID -> Res.drawable.ic_city_solid CategoryIcon.IC_CLINIC_MEDICAL_SOLID -> Res.drawable.ic_clinic_medical_solid CategoryIcon.IC_CLIPBOARD -> Res.drawable.ic_clipboard CategoryIcon.IC_CLOCK -> Res.drawable.ic_clock CategoryIcon.IC_CLOUD_DOWNLOAD_ALT_SOLID -> Res.drawable.ic_cloud_download_alt_solid CategoryIcon.IC_CLOUD_SOLID -> Res.drawable.ic_cloud_solid CategoryIcon.IC_CLOUD_UPLOAD_ALT_SOLID -> Res.drawable.ic_cloud_upload_alt_solid CategoryIcon.IC_COCKTAIL_SOLID -> Res.drawable.ic_cocktail_solid CategoryIcon.IC_CODE_BRANCH_SOLID -> Res.drawable.ic_code_branch_solid CategoryIcon.IC_CODE_SOLID -> Res.drawable.ic_code_solid CategoryIcon.IC_COFFEE_SOLID -> Res.drawable.ic_coffee_solid CategoryIcon.IC_COG_SOLID -> Res.drawable.ic_cog_solid CategoryIcon.IC_COINS_SOLID -> Res.drawable.ic_coins_solid CategoryIcon.IC_COMMENT_ALT -> Res.drawable.ic_comment_alt CategoryIcon.IC_COMPACT_DISC_SOLID -> Res.drawable.ic_compact_disc_solid CategoryIcon.IC_COMPASS -> Res.drawable.ic_compass CategoryIcon.IC_CONCIERGE_BELL_SOLID -> Res.drawable.ic_concierge_bell_solid CategoryIcon.IC_COOKIE_BITE_SOLID -> Res.drawable.ic_cookie_bite_solid CategoryIcon.IC_COUCH_SOLID -> Res.drawable.ic_couch_solid CategoryIcon.IC_CREDIT_CARD -> Res.drawable.ic_credit_card CategoryIcon.IC_CROWN_SOLID -> Res.drawable.ic_crown_solid CategoryIcon.IC_CUBES_SOLID -> Res.drawable.ic_cubes_solid CategoryIcon.IC_CUT_SOLID -> Res.drawable.ic_cut_solid CategoryIcon.IC_DESKTOP_SOLID -> Res.drawable.ic_desktop_solid CategoryIcon.IC_DIASPORA -> Res.drawable.ic_diaspora CategoryIcon.IC_DICE_D6_SOLID -> Res.drawable.ic_dice_d6_solid CategoryIcon.IC_DNA_SOLID -> Res.drawable.ic_dna_solid CategoryIcon.IC_DOG_SOLID -> Res.drawable.ic_dog_solid CategoryIcon.IC_DOLLAR_SIGN -> Res.drawable.ic_dollar_sign_solid CategoryIcon.IC_DOLLY_FLATBED_SOLID -> Res.drawable.ic_dolly_flatbed_solid CategoryIcon.IC_DOLLY_SOLID -> Res.drawable.ic_dolly_solid CategoryIcon.IC_DONATE_SOLID -> Res.drawable.ic_donate_solid CategoryIcon.IC_DRAFTING_COMPASS_SOLID -> Res.drawable.ic_drafting_compass_solid CategoryIcon.IC_DRUM_SOLID -> Res.drawable.ic_drum_solid CategoryIcon.IC_DRUMSTICK_BITE_SOLID -> Res.drawable.ic_drumstick_bite_solid CategoryIcon.IC_DUMBBELL_SOLID -> Res.drawable.ic_dumbbell_solid CategoryIcon.IC_DUMPSTER_SOLID -> Res.drawable.ic_dumpster_solid CategoryIcon.IC_EDIT -> Res.drawable.ic_edit CategoryIcon.IC_EGG_SOLID -> Res.drawable.ic_egg_solid CategoryIcon.IC_ENVELOPE -> Res.drawable.ic_envelope CategoryIcon.IC_ENVELOPE_OPEN -> Res.drawable.ic_envelope_open CategoryIcon.IC_ERASER_SOLID -> Res.drawable.ic_eraser_solid CategoryIcon.IC_EURO_SIGN -> Res.drawable.ic_euro_sign_solid CategoryIcon.IC_EXCHANGE_ALT_SOLID -> Res.drawable.ic_exchange_alt_solid CategoryIcon.IC_EXCLAMATION_CIRCLE_SOLID -> Res.drawable.ic_exclamation_circle_solid CategoryIcon.IC_EXCLAMATION_TRIANGLE_SOLID -> Res.drawable.ic_exclamation_triangle_solid CategoryIcon.IC_EXPEDITEDSSL -> Res.drawable.ic_expeditedssl CategoryIcon.IC_EXTERNAL_LINK_ALT_SOLID -> Res.drawable.ic_external_link_alt_solid CategoryIcon.IC_EYE_DROPPER_SOLID -> Res.drawable.ic_eye_dropper_solid CategoryIcon.IC_FAN_SOLID -> Res.drawable.ic_fan_solid CategoryIcon.IC_FAX_SOLID -> Res.drawable.ic_fax_solid CategoryIcon.IC_FEATHER_ALT_SOLID -> Res.drawable.ic_feather_alt_solid CategoryIcon.IC_FEMALE_SOLID -> Res.drawable.ic_female_solid CategoryIcon.IC_FIGHTER_JET_SOLID -> Res.drawable.ic_fighter_jet_solid CategoryIcon.IC_FILE -> Res.drawable.ic_file CategoryIcon.IC_FILE_ALT -> Res.drawable.ic_file_alt CategoryIcon.IC_FILE_AUDIO -> Res.drawable.ic_file_audio CategoryIcon.IC_FILE_CODE -> Res.drawable.ic_file_code CategoryIcon.IC_FILE_CSV_SOLID -> Res.drawable.ic_file_csv_solid CategoryIcon.IC_FILE_EXPORT_SOLID -> Res.drawable.ic_file_export_solid CategoryIcon.IC_FILE_IMPORT_SOLID -> Res.drawable.ic_file_import_solid CategoryIcon.IC_FILE_INVOICE_DOLLAR_SOLID -> Res.drawable.ic_file_invoice_dollar_solid CategoryIcon.IC_FILE_INVOICE_SOLID -> Res.drawable.ic_file_invoice_solid CategoryIcon.IC_FILE_PDF -> Res.drawable.ic_file_pdf CategoryIcon.IC_FILL_SOLID -> Res.drawable.ic_fill_solid CategoryIcon.IC_FILM_SOLID -> Res.drawable.ic_film_solid CategoryIcon.IC_FIRE_ALT_SOLID -> Res.drawable.ic_fire_alt_solid CategoryIcon.IC_FIRE_EXTINGUISHER_SOLID -> Res.drawable.ic_fire_extinguisher_solid CategoryIcon.IC_FIRST_AID_SOLID -> Res.drawable.ic_first_aid_solid CategoryIcon.IC_FISH_SOLID -> Res.drawable.ic_fish_solid CategoryIcon.IC_FLAG -> Res.drawable.ic_flag CategoryIcon.IC_FLAG_CHECKERED_SOLID -> Res.drawable.ic_flag_checkered_solid CategoryIcon.IC_FLASK_SOLID -> Res.drawable.ic_flask_solid CategoryIcon.IC_FLY -> Res.drawable.ic_fly CategoryIcon.IC_FOLDER -> Res.drawable.ic_folder CategoryIcon.IC_FOOTBALL_BALL_SOLID -> Res.drawable.ic_football_ball_solid CategoryIcon.IC_FORT_AWESOME -> Res.drawable.ic_fort_awesome CategoryIcon.IC_FROWN -> Res.drawable.ic_frown CategoryIcon.IC_FUTBOL -> Res.drawable.ic_futbol CategoryIcon.IC_GAMEPAD_SOLID -> Res.drawable.ic_gamepad_solid CategoryIcon.IC_GAS_PUMP_SOLID -> Res.drawable.ic_gas_pump_solid CategoryIcon.IC_GAVEL_SOLID -> Res.drawable.ic_gavel_solid CategoryIcon.IC_GIFT_SOLID -> Res.drawable.ic_gift_solid CategoryIcon.IC_GLASS_CHEERS_SOLID -> Res.drawable.ic_glass_cheers_solid CategoryIcon.IC_GLASS_MARTINI_ALT_SOLID -> Res.drawable.ic_glass_martini_alt_solid CategoryIcon.IC_GLOBE_SOLID -> Res.drawable.ic_globe_solid CategoryIcon.IC_GOLF_BALL_SOLID -> Res.drawable.ic_golf_ball_solid CategoryIcon.IC_GOPURAM_SOLID -> Res.drawable.ic_gopuram_solid CategoryIcon.IC_GRADUATION_CAP_SOLID -> Res.drawable.ic_graduation_cap_solid CategoryIcon.IC_GUITAR_SOLID -> Res.drawable.ic_guitar_solid CategoryIcon.IC_HAMBURGER_SOLID -> Res.drawable.ic_hamburger_solid CategoryIcon.IC_HAMMER_SOLID -> Res.drawable.ic_hammer_solid CategoryIcon.IC_HAT_COWBOY_SOLID -> Res.drawable.ic_hat_cowboy_solid CategoryIcon.IC_HDD -> Res.drawable.ic_hdd CategoryIcon.IC_HEADPHONES_SOLID -> Res.drawable.ic_headphones_solid CategoryIcon.IC_HELICOPTER_SOLID -> Res.drawable.ic_helicopter_solid CategoryIcon.IC_HIGHLIGHTER_SOLID -> Res.drawable.ic_highlighter_solid CategoryIcon.IC_HIKING_SOLID -> Res.drawable.ic_hiking_solid CategoryIcon.IC_HOME_SOLID -> Res.drawable.ic_home_solid CategoryIcon.IC_HORSE_HEAD_SOLID -> Res.drawable.ic_horse_head_solid CategoryIcon.IC_HOSPITAL -> Res.drawable.ic_hospital CategoryIcon.IC_HOTDOG_SOLID -> Res.drawable.ic_hotdog_solid CategoryIcon.IC_HOURGLASS_HALF_SOLID -> Res.drawable.ic_hourglass_half_solid CategoryIcon.IC_ICE_CREAM_SOLID -> Res.drawable.ic_ice_cream_solid CategoryIcon.IC_ID_CARD -> Res.drawable.ic_id_card CategoryIcon.IC_IMAGE -> Res.drawable.ic_image CategoryIcon.IC_INBOX_SOLID -> Res.drawable.ic_inbox_solid CategoryIcon.IC_INDUSTRY_SOLID -> Res.drawable.ic_industry_solid CategoryIcon.IC_ITUNES_NOTE -> Res.drawable.ic_itunes_note CategoryIcon.IC_KEY_SOLID -> Res.drawable.ic_key_solid CategoryIcon.IC_KEYBOARD -> Res.drawable.ic_keyboard CategoryIcon.IC_LANDMARK_SOLID -> Res.drawable.ic_landmark_solid CategoryIcon.IC_LAPTOP_SOLID -> Res.drawable.ic_laptop_solid CategoryIcon.IC_LIGHTBULB -> Res.drawable.ic_lightbulb CategoryIcon.IC_LIST_UL_SOLID -> Res.drawable.ic_list_ul_solid CategoryIcon.IC_LUGGAGE_CART_SOLID -> Res.drawable.ic_luggage_cart_solid CategoryIcon.IC_MAIL_BULK_SOLID -> Res.drawable.ic_mail_bulk_solid CategoryIcon.IC_MALE_SOLID -> Res.drawable.ic_male_solid CategoryIcon.IC_MAP_MARKED_ALT_SOLID -> Res.drawable.ic_map_marked_alt_solid CategoryIcon.IC_MARKER_SOLID -> Res.drawable.ic_marker_solid CategoryIcon.IC_MARS_SOLID -> Res.drawable.ic_mars_solid CategoryIcon.IC_MASK_SOLID -> Res.drawable.ic_mask_solid CategoryIcon.IC_MEDAL_SOLID -> Res.drawable.ic_medal_solid CategoryIcon.IC_MEDAPPS -> Res.drawable.ic_medapps CategoryIcon.IC_MEDKIT_SOLID -> Res.drawable.ic_medkit_solid CategoryIcon.IC_MERCURY_SOLID -> Res.drawable.ic_mercury_solid CategoryIcon.IC_MICROCHIP_SOLID -> Res.drawable.ic_microchip_solid CategoryIcon.IC_MICROPHONE_ALT_SOLID -> Res.drawable.ic_microphone_alt_solid CategoryIcon.IC_MICROSCOPE_SOLID -> Res.drawable.ic_microscope_solid CategoryIcon.IC_MOBILE_SOLID -> Res.drawable.ic_mobile_solid CategoryIcon.IC_MONEY_CHECK_ALT_SOLID -> Res.drawable.ic_money_check_alt_solid CategoryIcon.IC_MORTAR_PESTLE_SOLID -> Res.drawable.ic_mortar_pestle_solid CategoryIcon.IC_MOTORCYCLE_SOLID -> Res.drawable.ic_motorcycle_solid CategoryIcon.IC_MOUNTAIN_SOLID -> Res.drawable.ic_mountain_solid CategoryIcon.IC_MUG_HOT_SOLID -> Res.drawable.ic_mug_hot_solid CategoryIcon.IC_OIL_CAN_SOLID -> Res.drawable.ic_oil_can_solid CategoryIcon.IC_PAGER_SOLID -> Res.drawable.ic_pager_solid CategoryIcon.IC_PAINT_ROLLER_SOLID -> Res.drawable.ic_paint_roller_solid CategoryIcon.IC_PAPERCLIP_SOLID -> Res.drawable.ic_paperclip_solid CategoryIcon.IC_PARACHUTE_BOX_SOLID -> Res.drawable.ic_parachute_box_solid CategoryIcon.IC_PARKING_SOLID -> Res.drawable.ic_parking_solid CategoryIcon.IC_PASSPORT_SOLID -> Res.drawable.ic_passport_solid CategoryIcon.IC_PAW_SOLID -> Res.drawable.ic_paw_solid CategoryIcon.IC_PEN_ALT_SOLID -> Res.drawable.ic_pen_alt_solid CategoryIcon.IC_PEN_SOLID -> Res.drawable.ic_pen_solid CategoryIcon.IC_PHONE_SOLID -> Res.drawable.ic_phone_solid CategoryIcon.IC_PHOTO_VIDEO_SOLID -> Res.drawable.ic_photo_video_solid CategoryIcon.IC_PIGGY_BANK_SOLID -> Res.drawable.ic_piggy_bank_solid CategoryIcon.IC_PILLS_SOLID -> Res.drawable.ic_pills_solid CategoryIcon.IC_PIZZA_SLICE_SOLID -> Res.drawable.ic_pizza_slice_solid CategoryIcon.IC_PLANE_SOLID -> Res.drawable.ic_plane_solid CategoryIcon.IC_PLUG_SOLID -> Res.drawable.ic_plug_solid CategoryIcon.IC_POUND_SIGN_SOLID -> Res.drawable.ic_pound_sign_solid CategoryIcon.IC_PRESCRIPTION_BOTTLE_SOLID -> Res.drawable.ic_prescription_bottle_solid CategoryIcon.IC_QUESTION_CIRCLE -> Res.drawable.ic_question_circle CategoryIcon.IC_PRINT_SOLID -> Res.drawable.ic_print_solid CategoryIcon.IC_README -> Res.drawable.ic_readme CategoryIcon.IC_RECYCLE_SOLID -> Res.drawable.ic_recycle_solid CategoryIcon.IC_RESTROOM_SOLID -> Res.drawable.ic_restroom_solid CategoryIcon.IC_ROAD_SOLID -> Res.drawable.ic_road_solid CategoryIcon.IC_ROBOT_SOLID -> Res.drawable.ic_robot_solid CategoryIcon.IC_ROCKET_SOLID -> Res.drawable.ic_rocket_solid CategoryIcon.IC_RUNNING_SOLID -> Res.drawable.ic_running_solid CategoryIcon.IC_SCREWDRIVER_SOLID -> Res.drawable.ic_screwdriver_solid CategoryIcon.IC_SCROLL_SOLID -> Res.drawable.ic_scroll_solid CategoryIcon.IC_SEEDLING_SOLID -> Res.drawable.ic_seedling_solid CategoryIcon.IC_SERVER_SOLID -> Res.drawable.ic_server_solid CategoryIcon.IC_SHIELD_ALT_SOLID -> Res.drawable.ic_shield_alt_solid CategoryIcon.IC_SHIP_SOLID -> Res.drawable.ic_ship_solid CategoryIcon.IC_SHIPPING_FAST_SOLID -> Res.drawable.ic_shipping_fast_solid CategoryIcon.IC_SHOPPING_BAG_SOLID -> Res.drawable.ic_shopping_bag_solid CategoryIcon.IC_SHOPPING_CART_SOLID -> Res.drawable.ic_shopping_cart_solid CategoryIcon.IC_SHUTTLE_VAN_SOLID -> Res.drawable.ic_shuttle_van_solid CategoryIcon.IC_SIGNAL_SOLID -> Res.drawable.ic_signal_solid CategoryIcon.IC_SIM_CARD_SOLID -> Res.drawable.ic_sim_card_solid CategoryIcon.IC_SKATING_SOLID -> Res.drawable.ic_skating_solid CategoryIcon.IC_SKIING_NORDIC_SOLID -> Res.drawable.ic_skiing_nordic_solid CategoryIcon.IC_SKIING_SOLID -> Res.drawable.ic_skiing_solid CategoryIcon.IC_SMOKING_SOLID -> Res.drawable.ic_smoking_solid CategoryIcon.IC_SMS_SOLID -> Res.drawable.ic_sms_solid CategoryIcon.IC_SNOWBOARDING_SOLID -> Res.drawable.ic_snowboarding_solid CategoryIcon.IC_SNOWFLAKE -> Res.drawable.ic_snowflake CategoryIcon.IC_SOCKS_SOLID -> Res.drawable.ic_socks_solid CategoryIcon.IC_SPIDER_SOLID -> Res.drawable.ic_spider_solid CategoryIcon.IC_SPRAY_CAN_SOLID -> Res.drawable.ic_spray_can_solid CategoryIcon.IC_STAMP_SOLID -> Res.drawable.ic_stamp_solid CategoryIcon.IC_STAR_OF_LIFE_SOLID -> Res.drawable.ic_star_of_life_solid CategoryIcon.IC_STETHOSCOPE_SOLID -> Res.drawable.ic_stethoscope_solid CategoryIcon.IC_STICKY_NOTE -> Res.drawable.ic_sticky_note CategoryIcon.IC_STOPWATCH_SOLID -> Res.drawable.ic_stopwatch_solid CategoryIcon.IC_STORE_ALT_SOLID -> Res.drawable.ic_store_alt_solid CategoryIcon.IC_SUBWAY_SOLID -> Res.drawable.ic_subway_solid CategoryIcon.IC_SUITCASE_SOLID -> Res.drawable.ic_suitcase_solid CategoryIcon.IC_SWIMMER_SOLID -> Res.drawable.ic_swimmer_solid CategoryIcon.IC_SYRINGE_SOLID -> Res.drawable.ic_syringe_solid CategoryIcon.IC_TABLE_TENNIS_SOLID -> Res.drawable.ic_table_tennis_solid CategoryIcon.IC_TABLET_SOLID -> Res.drawable.ic_tablet_solid CategoryIcon.IC_TACHOMETER_ALT_SOLID -> Res.drawable.ic_tachometer_alt_solid CategoryIcon.IC_TAG_SOLID -> Res.drawable.ic_tag_solid CategoryIcon.IC_TAXI_SOLID -> Res.drawable.ic_taxi_solid CategoryIcon.IC_TEMPERATURE_HIGH_SOLID -> Res.drawable.ic_temperature_high_solid CategoryIcon.IC_TERMINAL_SOLID -> Res.drawable.ic_terminal_solid CategoryIcon.IC_THEATER_MASKS_SOLID -> Res.drawable.ic_theater_masks_solid CategoryIcon.IC_THERMOMETER_FULL_SOLID -> Res.drawable.ic_thermometer_full_solid CategoryIcon.IC_TICKET_ALT_SOLID -> Res.drawable.ic_ticket_alt_solid CategoryIcon.IC_TINT_SOLID -> Res.drawable.ic_tint_solid CategoryIcon.IC_TOILET_PAPER_SOLID -> Res.drawable.ic_toilet_paper_solid CategoryIcon.IC_TOOLBOX_SOLID -> Res.drawable.ic_toolbox_solid CategoryIcon.IC_TOOLS_SOLID -> Res.drawable.ic_tools_solid CategoryIcon.IC_TOOTH_SOLID -> Res.drawable.ic_tooth_solid CategoryIcon.IC_TRACTOR_SOLID -> Res.drawable.ic_tractor_solid CategoryIcon.IC_TRAIN_SOLID -> Res.drawable.ic_train_solid CategoryIcon.IC_TRASH_ALT -> Res.drawable.ic_trash_alt CategoryIcon.IC_TREE_SOLID -> Res.drawable.ic_tree_solid CategoryIcon.IC_TROPHY_SOLID -> Res.drawable.ic_trophy_solid CategoryIcon.IC_TRUCK_LOADING_SOLID -> Res.drawable.ic_truck_loading_solid CategoryIcon.IC_TRUCK_MOVING_SOLID -> Res.drawable.ic_truck_moving_solid CategoryIcon.IC_TRUCK_PICKUP_SOLID -> Res.drawable.ic_truck_pickup_solid CategoryIcon.IC_TSHIRT_SOLID -> Res.drawable.ic_tshirt_solid CategoryIcon.IC_TV_SOLID -> Res.drawable.ic_tv_solid CategoryIcon.IC_UNIVERSITY_SOLID -> Res.drawable.ic_university_solid CategoryIcon.IC_USER -> Res.drawable.ic_user CategoryIcon.IC_USER_FRIENDS_SOLID -> Res.drawable.ic_user_friends_solid CategoryIcon.IC_UTENSILS_SOLID -> Res.drawable.ic_utensils_solid CategoryIcon.IC_VENUS_SOLID -> Res.drawable.ic_venus_solid CategoryIcon.IC_VIAL_SOLID -> Res.drawable.ic_vial_solid CategoryIcon.IC_VIDEO_SOLID -> Res.drawable.ic_video_solid CategoryIcon.IC_VOLLEYBALL_BALL_SOLID -> Res.drawable.ic_volleyball_ball_solid CategoryIcon.IC_VOLUME_UP_SOLID -> Res.drawable.ic_volume_up_solid CategoryIcon.IC_WALKING_SOLID -> Res.drawable.ic_walking_solid CategoryIcon.IC_WALLET_SOLID -> Res.drawable.ic_wallet_solid CategoryIcon.IC_WINE_GLASS_SOLID -> Res.drawable.ic_wine_glass_solid CategoryIcon.IC_WRENCH_SOLID -> Res.drawable.ic_wrench_solid CategoryIcon.IC_YEN_SIGN_SOLID -> Res.drawable.ic_yen_sign_solid } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/categories/components/CategoryCard.kt ================================================ package com.prof18.moneyflow.presentation.categories.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.presentation.categories.mapToDrawableResource import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import org.jetbrains.compose.resources.painterResource @Composable internal fun CategoryCard( category: Category, onClick: ((Category) -> Unit)?, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.clickable(onClick = { onClick?.invoke(category) }, enabled = onClick != null), ) { // TODO is this weight necessary? @Suppress("MagicNumber") Row(modifier = Modifier.weight(8f)) { Box( modifier = Modifier .align(Alignment.CenterVertically) .padding( Margins.regular, ) .background( MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(Margins.regularCornerRadius), ), ) { Icon( painter = painterResource(category.icon.mapToDrawableResource()), contentDescription = null, modifier = Modifier .padding(Margins.small) .size(28.dp), tint = MaterialTheme.colorScheme.onPrimary, ) } Text( modifier = Modifier.align(Alignment.CenterVertically), text = category.name, style = MaterialTheme.typography.titleMedium, ) } } } @Preview(name = "CategoryCard Light") @Composable private fun CategoryCardPreview() { MoneyFlowTheme { Surface { CategoryCard( category = Category( id = 11, name = "Family", icon = CategoryIcon.IC_QUESTION_CIRCLE, type = TransactionType.OUTCOME, createdAtMillis = 1, ), onClick = {}, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/categories/data/CategoryUIData.kt ================================================ package com.prof18.moneyflow.presentation.categories.data import com.prof18.moneyflow.domain.entities.Category import com.prof18.moneyflow.presentation.model.CategoryIcon import kotlinx.serialization.Serializable @Serializable internal data class CategoryUIData( val id: Long, val name: String, val icon: CategoryIcon, ) internal fun Category.toCategoryUIData() = CategoryUIData( id = this.id, name = this.name, icon = this.icon, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/home/HomeModel.kt ================================================ package com.prof18.moneyflow.presentation.home import com.prof18.moneyflow.domain.entities.BalanceRecap import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.presentation.model.UIErrorMessage internal sealed class HomeModel { data object Loading : HomeModel() data class Error(val uiErrorMessage: UIErrorMessage) : HomeModel() data class HomeState( val balanceRecap: BalanceRecap, val latestTransactions: List, val currencyConfig: CurrencyConfig, ) : HomeModel() } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/home/HomeScreen.kt ================================================ package com.prof18.moneyflow.presentation.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import co.touchlab.kermit.Logger import com.prof18.moneyflow.domain.entities.BalanceRecap import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.domain.entities.TransactionTypeUI import com.prof18.moneyflow.presentation.home.components.HeaderNavigator import com.prof18.moneyflow.presentation.home.components.HomeRecap import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.presentation.model.UIErrorMessage import com.prof18.moneyflow.ui.components.ErrorView import com.prof18.moneyflow.ui.components.Loader import com.prof18.moneyflow.ui.components.TransactionCard import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.delete import money_flow.shared.generated.resources.empty_wallet import money_flow.shared.generated.resources.error_get_money_summary_message import money_flow.shared.generated.resources.hide_sensitive_data import money_flow.shared.generated.resources.latest_transactions import money_flow.shared.generated.resources.my_wallet import money_flow.shared.generated.resources.show_sensitive_data import money_flow.shared.generated.resources.shrug import org.jetbrains.compose.resources.stringResource @Composable @Suppress("LongMethod") // TODO: reduce method length internal fun HomeScreen( homeModel: HomeModel, hideSensitiveDataState: Boolean, navigateToAllTransactions: () -> Unit, paddingValues: PaddingValues, navigateToAddTransaction: () -> Unit = {}, deleteTransaction: (Long) -> Unit = {}, changeSensitiveDataVisibility: (Boolean) -> Unit = {}, ) { when (homeModel) { is HomeModel.Loading -> Loader() is HomeModel.HomeState -> { Column( modifier = Modifier .padding(paddingValues) .padding(Margins.small), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( text = stringResource(Res.string.my_wallet), style = MaterialTheme.typography.headlineLarge, modifier = Modifier .padding(horizontal = Margins.regular) .padding(top = Margins.regular), ) Row { IconButton( onClick = { changeSensitiveDataVisibility(hideSensitiveDataState.not()) }, modifier = Modifier .align(Alignment.CenterVertically) .padding(top = Margins.small), ) { if (hideSensitiveDataState) { Icon( Icons.Rounded.Visibility, contentDescription = stringResource(Res.string.show_sensitive_data), ) } else { Icon( Icons.Rounded.VisibilityOff, contentDescription = stringResource(Res.string.hide_sensitive_data), ) } } IconButton( onClick = { navigateToAddTransaction() }, modifier = Modifier .align(Alignment.CenterVertically) .padding(top = Margins.small), ) { Icon( Icons.Rounded.Add, contentDescription = null, ) } } } HomeRecap( balanceRecap = homeModel.balanceRecap, currencyConfig = homeModel.currencyConfig, hideSensitiveData = hideSensitiveDataState, ) HeaderNavigator( title = stringResource(Res.string.latest_transactions), onClick = navigateToAllTransactions, ) if (homeModel.latestTransactions.isEmpty()) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = Margins.regular) .padding(top = Margins.regular) .padding(bottom = paddingValues.calculateBottomPadding()), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( stringResource(Res.string.shrug), modifier = Modifier .padding(bottom = Margins.small), style = MaterialTheme.typography.headlineSmall, ) Text( stringResource(Res.string.empty_wallet), style = MaterialTheme.typography.headlineSmall, ) } } else { LazyColumn( modifier = Modifier .padding(bottom = paddingValues.calculateBottomPadding()), ) { items(homeModel.latestTransactions) { transaction -> val (showTransactionMenu, setShowTransactionMenu) = remember { mutableStateOf( false, ) } Box( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.TopStart), ) { TransactionCard( transaction = transaction, onClick = { Logger.d { "onClick" } }, onLongPress = { setShowTransactionMenu(true) }, hideSensitiveData = hideSensitiveDataState, currencyConfig = homeModel.currencyConfig, ) DropdownMenu( expanded = showTransactionMenu, onDismissRequest = { setShowTransactionMenu(false) }, ) { DropdownMenuItem( text = { Text(stringResource(Res.string.delete)) }, onClick = { deleteTransaction(transaction.id) setShowTransactionMenu(false) }, ) } } HorizontalDivider() } } } } } is HomeModel.Error -> ErrorView(uiErrorMessage = homeModel.uiErrorMessage) } } @Preview(name = "HomeScreen Light") @Composable private fun HomeScreenPreview() { MoneyFlowTheme { Surface { HomeScreen( homeModel = HomeModel.HomeState( balanceRecap = BalanceRecap( totalBalanceCents = 500_000, monthlyIncomeCents = 100_000, monthlyExpensesCents = 5_000, ), latestTransactions = listOf( MoneyTransaction( id = 0, title = "Ice Cream", icon = CategoryIcon.IC_ICE_CREAM_SOLID, amountCents = 1_000, type = TransactionTypeUI.EXPENSE, milliseconds = 0, formattedDate = "12 July 2021", ), MoneyTransaction( id = 1, title = "Tip", icon = CategoryIcon.IC_MONEY_CHECK_ALT_SOLID, amountCents = 5_000, type = TransactionTypeUI.INCOME, milliseconds = 0, formattedDate = "12 July 2021", ), ), currencyConfig = com.prof18.moneyflow.domain.entities.CurrencyConfig( code = "EUR", symbol = "€", decimalPlaces = 2, ), ), hideSensitiveDataState = true, navigateToAllTransactions = {}, paddingValues = PaddingValues(), ) } } } @Preview(name = "HomeScreenError Light") @Composable private fun HomeScreenErrorPreview() { MoneyFlowTheme { Surface { HomeScreen( homeModel = HomeModel.Error( UIErrorMessage( message = Res.string.error_get_money_summary_message, ), ), hideSensitiveDataState = true, navigateToAllTransactions = {}, paddingValues = PaddingValues(), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/home/components/HeaderNavigator.kt ================================================ package com.prof18.moneyflow.presentation.home.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme @Composable internal fun HeaderNavigator( title: String, onClick: () -> Unit = {}, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding(top = Margins.regular, bottom = Margins.small) .clickable { onClick() }, ) { Text( text = title, style = MaterialTheme.typography.headlineMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier .weight(1f) .padding(start = Margins.regular), ) IconButton( onClick = { /* no-op, managed by the row */ }, modifier = Modifier .align(Alignment.CenterVertically) .padding(vertical = Margins.small), ) { Icon( Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, ) } } } @Preview(name = "HeaderNavigator Light") @Composable private fun SnackCardPreview() { MoneyFlowTheme { Surface { HeaderNavigator(title = "This is a title") } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/home/components/HomeRecap.kt ================================================ package com.prof18.moneyflow.presentation.home.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.prof18.moneyflow.domain.entities.BalanceRecap import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.ui.components.HideableTextField import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.ui.style.downArrowCircleColor import com.prof18.moneyflow.ui.style.downArrowColor import com.prof18.moneyflow.ui.style.upArrowCircleColor import com.prof18.moneyflow.ui.style.upArrowColor import com.prof18.moneyflow.utils.formatAsCurrency import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.down_arrow_content_desc import money_flow.shared.generated.resources.ic_arrow_down_rotate import money_flow.shared.generated.resources.ic_arrow_up_rotate import money_flow.shared.generated.resources.total_balance import money_flow.shared.generated.resources.transaction_type_income import money_flow.shared.generated.resources.transaction_type_outcome import money_flow.shared.generated.resources.up_arrow_content_desc import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable @Suppress("LongMethod") // TODO: reduce method length internal fun HomeRecap( balanceRecap: BalanceRecap, currencyConfig: CurrencyConfig, hideSensitiveData: Boolean, ) { Column( modifier = Modifier .fillMaxWidth() .padding(Margins.regular), ) { Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { HideableTextField( text = balanceRecap.totalBalanceCents.formatAsCurrency(currencyConfig), hide = hideSensitiveData, style = MaterialTheme.typography.displaySmall, ) } Text( text = stringResource(Res.string.total_balance), style = MaterialTheme.typography.titleSmall, modifier = Modifier.align(Alignment.CenterHorizontally), ) Spacer(Modifier.height(Margins.medium)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { Row { Box( modifier = Modifier .align(Alignment.CenterVertically) .padding(end = Margins.regular) .background(upArrowCircleColor(), shape = CircleShape), ) { Icon( painter = painterResource(Res.drawable.ic_arrow_up_rotate), contentDescription = stringResource(Res.string.up_arrow_content_desc), modifier = Modifier .padding(Margins.small) .size(24.dp), tint = upArrowColor(), ) } Column { HideableTextField( text = balanceRecap.monthlyIncomeCents.formatAsCurrency(currencyConfig), hide = hideSensitiveData, style = MaterialTheme.typography.headlineMedium, ) Text( text = stringResource(Res.string.transaction_type_income), style = MaterialTheme.typography.titleSmall, ) } } Row { Box( modifier = Modifier .align(Alignment.CenterVertically) .padding(end = Margins.regular, start = Margins.medium) .background(downArrowCircleColor(), shape = CircleShape), ) { Icon( painter = painterResource(Res.drawable.ic_arrow_down_rotate), contentDescription = stringResource(Res.string.down_arrow_content_desc), modifier = Modifier .padding(Margins.small) .size(24.dp), tint = downArrowColor(), ) } Column { HideableTextField( text = balanceRecap.monthlyExpensesCents.formatAsCurrency(currencyConfig), hide = hideSensitiveData, style = MaterialTheme.typography.headlineMedium, ) Text( text = stringResource(Res.string.transaction_type_outcome), style = MaterialTheme.typography.titleSmall, modifier = Modifier.align(Alignment.End), ) } } } } } @Preview(name = "HomeRecap Light") @Composable private fun HomeRecapPreview() { MoneyFlowTheme { Surface { HomeRecap( balanceRecap = BalanceRecap( totalBalanceCents = 120_000, monthlyIncomeCents = 15_000, monthlyExpensesCents = 20_000, ), currencyConfig = CurrencyConfig( code = "EUR", symbol = "€", decimalPlaces = 2, ), hideSensitiveData = true, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/model/CategoryIcon.kt ================================================ @file:Suppress("TrailingCommaOnDeclarationSite") package com.prof18.moneyflow.presentation.model internal enum class CategoryIcon(val iconName: String) { IC_ADDRESS_BOOK("ic_address_book"), IC_ADDRESS_CARD("ic_address_card"), IC_ADJUST_SOLID("ic_adjust_solid"), IC_AIR_FRESHENER_SOLID("ic_air_freshener_solid"), IC_ALGOLIA("ic_algolia"), IC_ALLERGIES_SOLID("ic_allergies_solid"), IC_AMBULANCE_SOLID("ic_ambulance_solid"), IC_ANCHOR_SOLID("ic_anchor_solid"), IC_ANDROID("ic_android"), IC_ANGLE_DOWN_SOLID("ic_angle_down_solid"), IC_ANGLE_LEFT_SOLID("ic_angle_left_solid"), IC_ANGLE_RIGHT_SOLID("ic_angle_right_solid"), IC_ANGLE_UP_SOLID("ic_angle_up_solid"), IC_APPLE("ic_apple"), IC_APPLE_ALT_SOLID("ic_apple_alt_solid"), IC_ARCHIVE_SOLID("ic_archive_solid"), IC_ARCHWAY_SOLID("ic_archway_solid"), IC_ARROW_DOWN_SOLID("ic_arrow_down_solid"), IC_ARROW_LEFT_SOLID("ic_arrow_left_solid"), IC_ARROW_RIGHT_SOLID("ic_arrow_right_solid"), IC_ARROW_UP_SOLID("ic_arrow_up_solid"), IC_ASTERISK_SOLID("ic_asterisk_solid"), IC_AT_SOLID("ic_at_solid"), IC_ATLAS_SOLID("ic_atlas_solid"), IC_ATOM_SOLID("ic_atom_solid"), IC_AWARD_SOLID("ic_award_solid"), IC_BABY_CARRIAGE_SOLID("ic_baby_carriage_solid"), IC_BACON_SOLID("ic_bacon_solid"), IC_BALANCE_SCALE_LEFT_SOLID("ic_balance_scale_left_solid"), IC_BAND_AID_SOLID("ic_band_aid_solid"), IC_BASEBALL_BALL_SOLID("ic_baseball_ball_solid"), IC_BASKETBALL_BALL_SOLID("ic_basketball_ball_solid"), IC_BATH_SOLID("ic_bath_solid"), IC_BATTERY_THREE_QUARTERS_SOLID("ic_battery_three_quarters_solid"), IC_BED_SOLID("ic_bed_solid"), IC_BEER_SOLID("ic_beer_solid"), IC_BELL("ic_bell"), IC_BELL_SLASH("ic_bell_slash"), IC_BICYCLE_SOLID("ic_bicycle_solid"), IC_BIKING_SOLID("ic_biking_solid"), IC_BINOCULARS_SOLID("ic_binoculars_solid"), IC_BIRTHDAY_CAKE_SOLID("ic_birthday_cake_solid"), IC_BITCOIN("ic_bitcoin"), IC_BLACK_TIE("ic_black_tie"), IC_BLENDER_SOLID("ic_blender_solid"), IC_BLIND_SOLID("ic_blind_solid"), IC_BOLT_SOLID("ic_bolt_solid"), IC_BOMB_SOLID("ic_bomb_solid"), IC_BONE_SOLID("ic_bone_solid"), IC_BONG_SOLID("ic_bong_solid"), IC_BOOK_OPEN_SOLID("ic_book_open_solid"), IC_BOOK_SOLID("ic_book_solid"), IC_BOOKMARK("ic_bookmark"), IC_BOWLING_BALL_SOLID("ic_bowling_ball_solid"), IC_BOX_SOLID("ic_box_solid"), IC_BRAIN_SOLID("ic_brain_solid"), IC_BREAD_SLICE_SOLID("ic_bread_slice_solid"), IC_BRIEFCASE_MEDICAL_SOLID("ic_briefcase_medical_solid"), IC_BRIEFCASE_SOLID("ic_briefcase_solid"), IC_BROADCAST_TOWER_SOLID("ic_broadcast_tower_solid"), IC_BROOM_SOLID("ic_broom_solid"), IC_BRUSH_SOLID("ic_brush_solid"), IC_BUG_SOLID("ic_bug_solid"), IC_BUILDING("ic_building"), IC_BULLHORN_SOLID("ic_bullhorn_solid"), IC_BULLSEYE_SOLID("ic_bullseye_solid"), IC_BURN_SOLID("ic_burn_solid"), IC_BUS_SOLID("ic_bus_solid"), IC_CALCULATOR_SOLID("ic_calculator_solid"), IC_CALENDAR("ic_calendar"), IC_CAMERA_SOLID("ic_camera_solid"), IC_CAMPGROUND_SOLID("ic_campground_solid"), IC_CANDY_CANE_SOLID("ic_candy_cane_solid"), IC_CAPSULES_SOLID("ic_capsules_solid"), IC_CAR_ALT_SOLID("ic_car_alt_solid"), IC_CAR_SIDE_SOLID("ic_car_side_solid"), IC_CARET_DOWN_SOLID("ic_caret_down_solid"), IC_CARET_LEFT_SOLID("ic_caret_left_solid"), IC_CARET_RIGHT_SOLID("ic_caret_right_solid"), IC_CARET_UP_SOLID("ic_caret_up_solid"), IC_CARROT_SOLID("ic_carrot_solid"), IC_CART_ARROW_DOWN_SOLID("ic_cart_arrow_down_solid"), IC_CASH_REGISTER_SOLID("ic_cash_register_solid"), IC_CAT_SOLID("ic_cat_solid"), IC_CERTIFICATE_SOLID("ic_certificate_solid"), IC_CHAIR_SOLID("ic_chair_solid"), IC_CHALKBOARD_SOLID("ic_chalkboard_solid"), IC_CHALKBOARD_TEACHER_SOLID("ic_chalkboard_teacher_solid"), IC_CHARGING_STATION_SOLID("ic_charging_station_solid"), IC_CHART_AREA_SOLID("ic_chart_area_solid"), IC_CHART_BAR("ic_chart_bar"), IC_CHART_LINE_SOLID("ic_chart_line_solid"), IC_CHART_PIE_SOLID("ic_chart_pie_solid"), IC_CHECK_CIRCLE("ic_check_circle"), IC_CHEESE_SOLID("ic_cheese_solid"), IC_CHURCH_SOLID("ic_church_solid"), IC_CITY_SOLID("ic_city_solid"), IC_CLINIC_MEDICAL_SOLID("ic_clinic_medical_solid"), IC_CLIPBOARD("ic_clipboard"), IC_CLOCK("ic_clock"), IC_CLOUD_DOWNLOAD_ALT_SOLID("ic_cloud_download_alt_solid"), IC_CLOUD_SOLID("ic_cloud_solid"), IC_CLOUD_UPLOAD_ALT_SOLID("ic_cloud_upload_alt_solid"), IC_COCKTAIL_SOLID("ic_cocktail_solid"), IC_CODE_BRANCH_SOLID("ic_code_branch_solid"), IC_CODE_SOLID("ic_code_solid"), IC_COFFEE_SOLID("ic_coffee_solid"), IC_COG_SOLID("ic_cog_solid"), IC_COINS_SOLID("ic_coins_solid"), IC_COMMENT_ALT("ic_comment_alt"), IC_COMPACT_DISC_SOLID("ic_compact_disc_solid"), IC_COMPASS("ic_compass"), IC_CONCIERGE_BELL_SOLID("ic_concierge_bell_solid"), IC_COOKIE_BITE_SOLID("ic_cookie_bite_solid"), IC_COUCH_SOLID("ic_couch_solid"), IC_CREDIT_CARD("ic_credit_card"), IC_CROWN_SOLID("ic_crown_solid"), IC_CUBES_SOLID("ic_cubes_solid"), IC_CUT_SOLID("ic_cut_solid"), IC_DESKTOP_SOLID("ic_desktop_solid"), IC_DIASPORA("ic_diaspora"), IC_DICE_D6_SOLID("ic_dice_d6_solid"), IC_DNA_SOLID("ic_dna_solid"), IC_DOG_SOLID("ic_dog_solid"), IC_DOLLAR_SIGN("ic_dollar_sign"), IC_DOLLY_FLATBED_SOLID("ic_dolly_flatbed_solid"), IC_DOLLY_SOLID("ic_dolly_solid"), IC_DONATE_SOLID("ic_donate_solid"), IC_DRAFTING_COMPASS_SOLID("ic_drafting_compass_solid"), IC_DRUM_SOLID("ic_drum_solid"), IC_DRUMSTICK_BITE_SOLID("ic_drumstick_bite_solid"), IC_DUMBBELL_SOLID("ic_dumbbell_solid"), IC_DUMPSTER_SOLID("ic_dumpster_solid"), IC_EDIT("ic_edit"), IC_EGG_SOLID("ic_egg_solid"), IC_ENVELOPE("ic_envelope"), IC_ENVELOPE_OPEN("ic_envelope_open"), IC_ERASER_SOLID("ic_eraser_solid"), IC_EURO_SIGN("ic_euro_sign"), IC_EXCHANGE_ALT_SOLID("ic_exchange_alt_solid"), IC_EXCLAMATION_CIRCLE_SOLID("ic_exclamation_circle_solid"), IC_EXCLAMATION_TRIANGLE_SOLID("ic_exclamation_triangle_solid"), IC_EXPEDITEDSSL("ic_expeditedssl"), IC_EXTERNAL_LINK_ALT_SOLID("ic_external_link_alt_solid"), IC_EYE_DROPPER_SOLID("ic_eye_dropper_solid"), IC_FAN_SOLID("ic_fan_solid"), IC_FAX_SOLID("ic_fax_solid"), IC_FEATHER_ALT_SOLID("ic_feather_alt_solid"), IC_FEMALE_SOLID("ic_female_solid"), IC_FIGHTER_JET_SOLID("ic_fighter_jet_solid"), IC_FILE("ic_file"), IC_FILE_ALT("ic_file_alt"), IC_FILE_AUDIO("ic_file_audio"), IC_FILE_CODE("ic_file_code"), IC_FILE_CSV_SOLID("ic_file_csv_solid"), IC_FILE_EXPORT_SOLID("ic_file_export_solid"), IC_FILE_IMPORT_SOLID("ic_file_import_solid"), IC_FILE_INVOICE_DOLLAR_SOLID("ic_file_invoice_dollar_solid"), IC_FILE_INVOICE_SOLID("ic_file_invoice_solid"), IC_FILE_PDF("ic_file_pdf"), IC_FILL_SOLID("ic_fill_solid"), IC_FILM_SOLID("ic_film_solid"), IC_FIRE_ALT_SOLID("ic_fire_alt_solid"), IC_FIRE_EXTINGUISHER_SOLID("ic_fire_extinguisher_solid"), IC_FIRST_AID_SOLID("ic_first_aid_solid"), IC_FISH_SOLID("ic_fish_solid"), IC_FLAG("ic_flag"), IC_FLAG_CHECKERED_SOLID("ic_flag_checkered_solid"), IC_FLASK_SOLID("ic_flask_solid"), IC_FLY("ic_fly"), IC_FOLDER("ic_folder"), IC_FOOTBALL_BALL_SOLID("ic_football_ball_solid"), IC_FORT_AWESOME("ic_fort_awesome"), IC_FROWN("ic_frown"), IC_FUTBOL("ic_futbol"), IC_GAMEPAD_SOLID("ic_gamepad_solid"), IC_GAS_PUMP_SOLID("ic_gas_pump_solid"), IC_GAVEL_SOLID("ic_gavel_solid"), IC_GIFT_SOLID("ic_gift_solid"), IC_GLASS_CHEERS_SOLID("ic_glass_cheers_solid"), IC_GLASS_MARTINI_ALT_SOLID("ic_glass_martini_alt_solid"), IC_GLOBE_SOLID("ic_globe_solid"), IC_GOLF_BALL_SOLID("ic_golf_ball_solid"), IC_GOPURAM_SOLID("ic_gopuram_solid"), IC_GRADUATION_CAP_SOLID("ic_graduation_cap_solid"), IC_GUITAR_SOLID("ic_guitar_solid"), IC_HAMBURGER_SOLID("ic_hamburger_solid"), IC_HAMMER_SOLID("ic_hammer_solid"), IC_HAT_COWBOY_SOLID("ic_hat_cowboy_solid"), IC_HDD("ic_hdd"), IC_HEADPHONES_SOLID("ic_headphones_solid"), IC_HELICOPTER_SOLID("ic_helicopter_solid"), IC_HIGHLIGHTER_SOLID("ic_highlighter_solid"), IC_HIKING_SOLID("ic_hiking_solid"), IC_HOME_SOLID("ic_home_solid"), IC_HORSE_HEAD_SOLID("ic_horse_head_solid"), IC_HOSPITAL("ic_hospital"), IC_HOTDOG_SOLID("ic_hotdog_solid"), IC_HOURGLASS_HALF_SOLID("ic_hourglass_half_solid"), IC_ICE_CREAM_SOLID("ic_ice_cream_solid"), IC_ID_CARD("ic_id_card"), IC_IMAGE("ic_image"), IC_INBOX_SOLID("ic_inbox_solid"), IC_INDUSTRY_SOLID("ic_industry_solid"), IC_ITUNES_NOTE("ic_itunes_note"), IC_KEY_SOLID("ic_key_solid"), IC_KEYBOARD("ic_keyboard"), IC_LANDMARK_SOLID("ic_landmark_solid"), IC_LAPTOP_SOLID("ic_laptop_solid"), IC_LIGHTBULB("ic_lightbulb"), IC_LIST_UL_SOLID("ic_list_ul_solid"), IC_LUGGAGE_CART_SOLID("ic_luggage_cart_solid"), IC_MAIL_BULK_SOLID("ic_mail_bulk_solid"), IC_MALE_SOLID("ic_male_solid"), IC_MAP_MARKED_ALT_SOLID("ic_map_marked_alt_solid"), IC_MARKER_SOLID("ic_marker_solid"), IC_MARS_SOLID("ic_mars_solid"), IC_MASK_SOLID("ic_mask_solid"), IC_MEDAL_SOLID("ic_medal_solid"), IC_MEDAPPS("ic_medapps"), IC_MEDKIT_SOLID("ic_medkit_solid"), IC_MERCURY_SOLID("ic_mercury_solid"), IC_MICROCHIP_SOLID("ic_microchip_solid"), IC_MICROPHONE_ALT_SOLID("ic_microphone_alt_solid"), IC_MICROSCOPE_SOLID("ic_microscope_solid"), IC_MOBILE_SOLID("ic_mobile_solid"), IC_MONEY_CHECK_ALT_SOLID("ic_money_check_alt_solid"), IC_MORTAR_PESTLE_SOLID("ic_mortar_pestle_solid"), IC_MOTORCYCLE_SOLID("ic_motorcycle_solid"), IC_MOUNTAIN_SOLID("ic_mountain_solid"), IC_MUG_HOT_SOLID("ic_mug_hot_solid"), IC_OIL_CAN_SOLID("ic_oil_can_solid"), IC_PAGER_SOLID("ic_pager_solid"), IC_PAINT_ROLLER_SOLID("ic_paint_roller_solid"), IC_PAPERCLIP_SOLID("ic_paperclip_solid"), IC_PARACHUTE_BOX_SOLID("ic_parachute_box_solid"), IC_PARKING_SOLID("ic_parking_solid"), IC_PASSPORT_SOLID("ic_passport_solid"), IC_PAW_SOLID("ic_paw_solid"), IC_PEN_ALT_SOLID("ic_pen_alt_solid"), IC_PEN_SOLID("ic_pen_solid"), IC_PHONE_SOLID("ic_phone_solid"), IC_PHOTO_VIDEO_SOLID("ic_photo_video_solid"), IC_PIGGY_BANK_SOLID("ic_piggy_bank_solid"), IC_PILLS_SOLID("ic_pills_solid"), IC_PIZZA_SLICE_SOLID("ic_pizza_slice_solid"), IC_PLANE_SOLID("ic_plane_solid"), IC_PLUG_SOLID("ic_plug_solid"), IC_POUND_SIGN_SOLID("ic_pound_sign_solid"), IC_PRESCRIPTION_BOTTLE_SOLID("ic_prescription_bottle_solid"), IC_PRINT_SOLID("ic_print_solid"), IC_QUESTION_CIRCLE("ic_question_circle"), IC_README("ic_readme"), IC_RECYCLE_SOLID("ic_recycle_solid"), IC_RESTROOM_SOLID("ic_restroom_solid"), IC_ROAD_SOLID("ic_road_solid"), IC_ROBOT_SOLID("ic_robot_solid"), IC_ROCKET_SOLID("ic_rocket_solid"), IC_RUNNING_SOLID("ic_running_solid"), IC_SCREWDRIVER_SOLID("ic_screwdriver_solid"), IC_SCROLL_SOLID("ic_scroll_solid"), IC_SEEDLING_SOLID("ic_seedling_solid"), IC_SERVER_SOLID("ic_server_solid"), IC_SHIELD_ALT_SOLID("ic_shield_alt_solid"), IC_SHIP_SOLID("ic_ship_solid"), IC_SHIPPING_FAST_SOLID("ic_shipping_fast_solid"), IC_SHOPPING_BAG_SOLID("ic_shopping_bag_solid"), IC_SHOPPING_CART_SOLID("ic_shopping_cart_solid"), IC_SHUTTLE_VAN_SOLID("ic_shuttle_van_solid"), IC_SIGNAL_SOLID("ic_signal_solid"), IC_SIM_CARD_SOLID("ic_sim_card_solid"), IC_SKATING_SOLID("ic_skating_solid"), IC_SKIING_NORDIC_SOLID("ic_skiing_nordic_solid"), IC_SKIING_SOLID("ic_skiing_solid"), IC_SMOKING_SOLID("ic_smoking_solid"), IC_SMS_SOLID("ic_sms_solid"), IC_SNOWBOARDING_SOLID("ic_snowboarding_solid"), IC_SNOWFLAKE("ic_snowflake"), IC_SOCKS_SOLID("ic_socks_solid"), IC_SPIDER_SOLID("ic_spider_solid"), IC_SPRAY_CAN_SOLID("ic_spray_can_solid"), IC_STAMP_SOLID("ic_stamp_solid"), IC_STAR_OF_LIFE_SOLID("ic_star_of_life_solid"), IC_STETHOSCOPE_SOLID("ic_stethoscope_solid"), IC_STICKY_NOTE("ic_sticky_note"), IC_STOPWATCH_SOLID("ic_stopwatch_solid"), IC_STORE_ALT_SOLID("ic_store_alt_solid"), IC_SUBWAY_SOLID("ic_subway_solid"), IC_SUITCASE_SOLID("ic_suitcase_solid"), IC_SWIMMER_SOLID("ic_swimmer_solid"), IC_SYRINGE_SOLID("ic_syringe_solid"), IC_TABLE_TENNIS_SOLID("ic_table_tennis_solid"), IC_TABLET_SOLID("ic_tablet_solid"), IC_TACHOMETER_ALT_SOLID("ic_tachometer_alt_solid"), IC_TAG_SOLID("ic_tag_solid"), IC_TAXI_SOLID("ic_taxi_solid"), IC_TEMPERATURE_HIGH_SOLID("ic_temperature_high_solid"), IC_TERMINAL_SOLID("ic_terminal_solid"), IC_THEATER_MASKS_SOLID("ic_theater_masks_solid"), IC_THERMOMETER_FULL_SOLID("ic_thermometer_full_solid"), IC_TICKET_ALT_SOLID("ic_ticket_alt_solid"), IC_TINT_SOLID("ic_tint_solid"), IC_TOILET_PAPER_SOLID("ic_toilet_paper_solid"), IC_TOOLBOX_SOLID("ic_toolbox_solid"), IC_TOOLS_SOLID("ic_tools_solid"), IC_TOOTH_SOLID("ic_tooth_solid"), IC_TRACTOR_SOLID("ic_tractor_solid"), IC_TRAIN_SOLID("ic_train_solid"), IC_TRASH_ALT("ic_trash_alt"), IC_TREE_SOLID("ic_tree_solid"), IC_TROPHY_SOLID("ic_trophy_solid"), IC_TRUCK_LOADING_SOLID("ic_truck_loading_solid"), IC_TRUCK_MOVING_SOLID("ic_truck_moving_solid"), IC_TRUCK_PICKUP_SOLID("ic_truck_pickup_solid"), IC_TSHIRT_SOLID("ic_tshirt_solid"), IC_TV_SOLID("ic_tv_solid"), IC_UNIVERSITY_SOLID("ic_university_solid"), IC_USER("ic_user"), IC_USER_FRIENDS_SOLID("ic_user_friends_solid"), IC_UTENSILS_SOLID("ic_utensils_solid"), IC_VENUS_SOLID("ic_venus_solid"), IC_VIAL_SOLID("ic_vial_solid"), IC_VIDEO_SOLID("ic_video_solid"), IC_VOLLEYBALL_BALL_SOLID("ic_volleyball_ball_solid"), IC_VOLUME_UP_SOLID("ic_volume_up_solid"), IC_WALKING_SOLID("ic_walking_solid"), IC_WALLET_SOLID("ic_wallet_solid"), IC_WINE_GLASS_SOLID("ic_wine_glass_solid"), IC_WRENCH_SOLID("ic_wrench_solid"), IC_YEN_SIGN_SOLID("ic_yen_sign_solid"); companion object { fun fromValue(value: String): CategoryIcon = value .let { values().firstOrNull { v -> v.iconName == value } ?: IC_QUESTION_CIRCLE } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/model/UIErrorMessage.kt ================================================ package com.prof18.moneyflow.presentation.model import org.jetbrains.compose.resources.StringResource internal data class UIErrorMessage( val message: StringResource, ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/model/UIErrorMessageFactory.kt ================================================ package com.prof18.moneyflow.presentation.model import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.amount_not_empty_error import money_flow.shared.generated.resources.database_file_not_found import money_flow.shared.generated.resources.error_add_transaction_message import money_flow.shared.generated.resources.error_database_export import money_flow.shared.generated.resources.error_database_import import money_flow.shared.generated.resources.error_delete_transaction_message import money_flow.shared.generated.resources.error_get_all_transaction_message import money_flow.shared.generated.resources.error_get_categories_message import money_flow.shared.generated.resources.error_get_money_summary_message import org.jetbrains.compose.resources.StringResource internal fun uiErrorMessageFromKeys( messageKey: String, ): UIErrorMessage = UIErrorMessage( message = messageKey.toStringResource(), ) internal fun String.toStringResource(): StringResource = when (this) { "amount_not_empty_error" -> Res.string.amount_not_empty_error "error_add_transaction_message" -> Res.string.error_add_transaction_message "error_delete_transaction_message" -> Res.string.error_delete_transaction_message "error_get_all_transaction_message" -> Res.string.error_get_all_transaction_message "error_get_categories_message" -> Res.string.error_get_categories_message "error_get_money_summary_message" -> Res.string.error_get_money_summary_message "error_database_export" -> Res.string.error_database_export "error_database_import" -> Res.string.error_database_import "database_file_not_found" -> Res.string.database_file_not_found else -> Res.string.error_get_money_summary_message } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/recap/RecapScreen.kt ================================================ package com.prof18.moneyflow.presentation.recap import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @Composable internal fun RecapScreen() { Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { Text("Coming Soon") } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/settings/SettingsScreen.kt ================================================ package com.prof18.moneyflow.presentation.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.prof18.moneyflow.ui.components.SwitchWithText import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.biometric_support import money_flow.shared.generated.resources.hide_sensitive_data import money_flow.shared.generated.resources.security import money_flow.shared.generated.resources.settings_screen import org.jetbrains.compose.resources.stringResource @Composable internal fun SettingsScreen( biometricAvailabilityChecker: BiometricAvailabilityChecker, biometricState: Boolean, onBiometricEnabled: (Boolean) -> Unit, hideSensitiveDataState: Boolean, onHideSensitiveDataEnabled: (Boolean) -> Unit, paddingValues: PaddingValues, ) { SettingsScreenContent( isBiometricSupported = biometricAvailabilityChecker.isBiometricSupported(), biometricState = biometricState, onBiometricEnabled = onBiometricEnabled, hideSensitiveDataState = hideSensitiveDataState, onHideSensitiveDataEnabled = onHideSensitiveDataEnabled, paddingValues = paddingValues, ) } @Composable @Suppress("LongMethod") // TODO: reduce method length private fun SettingsScreenContent( isBiometricSupported: Boolean, biometricState: Boolean, onBiometricEnabled: (Boolean) -> Unit, hideSensitiveDataState: Boolean, onHideSensitiveDataEnabled: (Boolean) -> Unit, paddingValues: PaddingValues, ) { Scaffold( modifier = Modifier.padding(paddingValues), topBar = { Text( text = stringResource(Res.string.settings_screen), style = MaterialTheme.typography.headlineLarge, modifier = Modifier .padding(horizontal = Margins.regular) .padding(top = Margins.regular), ) }, content = { Column( modifier = Modifier .padding(paddingValues) .padding(top = Margins.regular), ) { Text( text = stringResource(Res.string.security), style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = Margins.regular), ) SwitchWithText( onSwitchChanged = onHideSensitiveDataEnabled, switchStatus = hideSensitiveDataState, title = stringResource(Res.string.hide_sensitive_data), ) if (isBiometricSupported) { SwitchWithText( onSwitchChanged = onBiometricEnabled, switchStatus = biometricState, title = stringResource(Res.string.biometric_support), ) } } }, ) } @Preview(name = "Settings Light") @Composable private fun SettingsScreenPreview() { MoneyFlowTheme { Surface { SettingsScreenContent( biometricState = true, isBiometricSupported = true, onBiometricEnabled = {}, hideSensitiveDataState = true, onHideSensitiveDataEnabled = {}, paddingValues = PaddingValues(), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/ArrowCircleIcon.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.ui.style.upArrowCircleColor import com.prof18.moneyflow.ui.style.upArrowColor import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_arrow_up_rotate import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @Composable internal fun ArrowCircleIcon( boxColor: Color, iconResource: DrawableResource, arrowColor: Color, iconSize: Dp, modifier: Modifier = Modifier, ) { Box( modifier = modifier .background( boxColor, shape = CircleShape, ), ) { Icon( painter = painterResource(iconResource), contentDescription = null, modifier = Modifier .padding(Margins.small) .size(iconSize), tint = arrowColor, ) } } @Preview(name = "ArrowCircleIcon Light") @Composable private fun ArrowCircleIconPreview() { Surface { MoneyFlowTheme { ArrowCircleIcon( boxColor = upArrowCircleColor(), iconResource = Res.drawable.ic_arrow_up_rotate, arrowColor = upArrowColor(), iconSize = 18.dp, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/ErrorView.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.presentation.model.UIErrorMessage import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.error_add_transaction_message import money_flow.shared.generated.resources.shrug import org.jetbrains.compose.resources.stringResource @Composable internal fun ErrorView( uiErrorMessage: UIErrorMessage, ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(Margins.regular), ) { Text( text = stringResource(Res.string.shrug), style = MaterialTheme.typography.bodySmall, ) Text( text = stringResource(uiErrorMessage.message), style = MaterialTheme.typography.bodyLarge, ) } } } @Preview(name = "ErrorView Light") @Composable private fun ErrorViewPreview() { val message = UIErrorMessage( message = Res.string.error_add_transaction_message, ) Surface { MoneyFlowTheme { ErrorView(uiErrorMessage = message) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/HideableTextField.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.MoneyFlowTheme @Composable internal fun HideableTextField( text: String, hide: Boolean, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.bodyLarge, ) { val hiddenWord: String by remember { mutableStateOf(text.replace("[\\d|.,]".toRegex(), "*")) } Text( modifier = modifier, text = if (hide) { hiddenWord } else { text }, style = style, ) } @Preview(name = "HideableTextFieldVisible Light") @Composable private fun HideableTextFieldVisiblePreview() { MoneyFlowTheme { Surface { HideableTextField( text = "$ 10.000", hide = true, ) } } } @Preview(name = "HideableTextFieldHidden Light") @Composable private fun HideableTextFieldHiddenPreview() { MoneyFlowTheme { Surface { HideableTextField( text = "$ 10.000", hide = false, ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/Loader.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.MoneyFlowTheme @Composable internal fun Loader() { Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), ) { CircularProgressIndicator() } } @Preview(name = "Loader Light") @Composable private fun LoaderPreview() { Surface { MoneyFlowTheme { Loader() } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/MFTopBar.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Close import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun MFTopBar( topAppBarText: String, actionTitle: String? = null, onBackPressed: (() -> Unit)? = null, onActionClicked: (() -> Unit)? = null, actionEnabled: Boolean = true, ) { TopAppBar( title = { Text( text = topAppBarText, style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp), ) }, navigationIcon = if (onBackPressed != null) { { IconButton(onClick = onBackPressed) { Icon( Icons.Rounded.Close, contentDescription = null, ) } } } else { {} }, actions = { if (onActionClicked != null) { Spacer(modifier = Modifier.width(68.dp)) TextButton(onClick = onActionClicked, enabled = actionEnabled) { Text( actionTitle!!.uppercase(), style = MaterialTheme.typography.titleSmall, ) } } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.onBackground, actionIconContentColor = MaterialTheme.colorScheme.onBackground, navigationIconContentColor = MaterialTheme.colorScheme.onBackground, ), ) } @Preview(name = "AddTransactionTopBar Light") @Composable private fun AddTransactionTopBarPreview() { return MFTopBar( topAppBarText = "Title", actionTitle = "Save", onBackPressed = { }, onActionClicked = { }, actionEnabled = false, ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/SwitchWithText.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme @Composable internal fun SwitchWithText( onSwitchChanged: (Boolean) -> Unit, switchStatus: Boolean, title: String, titleStyle: TextStyle = MaterialTheme.typography.titleSmall, ) { Row( modifier = Modifier .fillMaxWidth() .padding(end = Margins.regular), verticalAlignment = Alignment.CenterVertically, ) { @Suppress("MagicNumber") Text( text = title, style = titleStyle, modifier = Modifier .weight(0.9f) .clickable { onSwitchChanged(switchStatus.not()) } .padding(Margins.regular), ) @Suppress("MagicNumber") Switch( modifier = Modifier.weight(0.1f), checked = switchStatus, onCheckedChange = { onSwitchChanged(it) }, ) } } @Preview(name = "SwitchWithText Light") @Composable private fun SwitchWithTextPreview() { MoneyFlowTheme { Surface { SwitchWithText( onSwitchChanged = {}, switchStatus = true, title = "A super dupe preference", ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/components/TransactionCard.kt ================================================ package com.prof18.moneyflow.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.prof18.moneyflow.domain.entities.CurrencyConfig import com.prof18.moneyflow.domain.entities.MoneyTransaction import com.prof18.moneyflow.domain.entities.TransactionTypeUI import com.prof18.moneyflow.presentation.categories.mapToDrawableResource import com.prof18.moneyflow.presentation.model.CategoryIcon import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import com.prof18.moneyflow.ui.style.downArrowCircleColor import com.prof18.moneyflow.ui.style.downArrowColor import com.prof18.moneyflow.ui.style.upArrowCircleColor import com.prof18.moneyflow.ui.style.upArrowColor import com.prof18.moneyflow.utils.formatAsCurrency import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.ic_arrow_down_rotate import money_flow.shared.generated.resources.ic_arrow_up_rotate import org.jetbrains.compose.resources.painterResource @Composable @Suppress("LongMethod") // TODO: reduce method length internal fun TransactionCard( transaction: MoneyTransaction, onLongPress: () -> Unit, onClick: () -> Unit, hideSensitiveData: Boolean, currencyConfig: CurrencyConfig, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() .clickable(onClick = { onClick() }) .pointerInput(Unit) { detectTapGestures( onLongPress = { onLongPress() }, ) }, ) { Row { Box( modifier = Modifier .align(Alignment.CenterVertically) .padding( Margins.regular, ) .background( MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(Margins.regularCornerRadius), ), ) { Icon( painter = painterResource(transaction.icon.mapToDrawableResource()), contentDescription = null, modifier = Modifier .padding(Margins.small) .size(28.dp), tint = MaterialTheme.colorScheme.onPrimary, ) } Column( modifier = Modifier .align(Alignment.CenterVertically) .padding( top = Margins.regular, bottom = Margins.regular, end = Margins.regular, ), ) { Text( text = transaction.title, style = MaterialTheme.typography.titleMedium, ) Text( text = transaction.formattedDate, style = MaterialTheme.typography.bodySmall, ) } } var boxColor = upArrowCircleColor() var arrowColor = upArrowColor() var arrowIconResource = Res.drawable.ic_arrow_up_rotate if (transaction.type == TransactionTypeUI.EXPENSE) { boxColor = downArrowCircleColor() arrowColor = downArrowColor() arrowIconResource = Res.drawable.ic_arrow_down_rotate } Row( modifier = Modifier.align(Alignment.CenterVertically), ) { ArrowCircleIcon( boxColor = boxColor, iconResource = arrowIconResource, arrowColor = arrowColor, iconSize = 18.dp, modifier = Modifier.align(Alignment.CenterVertically), ) val signedAmount = if (transaction.type == TransactionTypeUI.EXPENSE) { -transaction.amountCents } else { transaction.amountCents } HideableTextField( text = signedAmount.formatAsCurrency(currencyConfig), style = MaterialTheme.typography.bodyLarge, modifier = Modifier .align(Alignment.CenterVertically) .padding(Margins.regular), hide = hideSensitiveData, ) } } } @Preview(name = "TransactionCard Light") @Composable private fun TransactionCardPreview() { Surface { MoneyFlowTheme { TransactionCard( transaction = MoneyTransaction( id = 0, title = "Eating out", icon = CategoryIcon.IC_HAMBURGER_SOLID, amountCents = 3_000, type = TransactionTypeUI.EXPENSE, milliseconds = 0, formattedDate = "12/12/21", ), onLongPress = {}, onClick = {}, hideSensitiveData = true, currencyConfig = CurrencyConfig( code = "EUR", symbol = "€", decimalPlaces = 2, ), ) } } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/style/Color.kt ================================================ package com.prof18.moneyflow.ui.style import androidx.compose.ui.graphics.Color internal object LightAppColors { val primary = Color(0XFF3E6275) val lightGrey = Color(0XFFF8F5F5) val background = Color(0xFFFAFAFA) val red1 = Color(0xFFFF464F) val red2 = Color(0xFFFF575F) val red3 = Color(0xFFFFE5E7) val orange1 = Color(0xFFFF8A34) val orange2 = Color(0xFFFF974A) val orange3 = Color(0xFFFFEFE3) val yellow1 = Color(0xFFFFBC25) val yellow2 = Color(0xFFFFC542) val yellow3 = Color(0xFFFEF3D9) val green1 = Color(0xFF25C685) val green2 = Color(0xFF3DD598) val green3 = Color(0xFFD4F5E9) val blue1 = Color(0xFF005DF2) val blue2 = Color(0xFF0062FF) val blue3 = Color(0xFFE3EEFF) val purple1 = Color(0xFF6952DC) val purple2 = Color(0xFF755FE2) val purple3 = Color(0xFFEDEAFD) val gray1 = Color(0xFF1A3B34) val gray2 = Color(0xFF899A96) val gray3 = Color(0xFFE4E9F3) val gray4 = Color(0xFFEDF1FA) } internal object DarkAppColors { val primary = Color(0xFF2C4653) val backgroundColor = Color(0XFF303030) val red1 = Color(0xFFFF464F) val red2 = Color(0xFFFF575F) val red3 = Color(0xFF623A42) val orange1 = Color(0xFFFF8A34) val orange2 = Color(0xFFFF974A) val orange3 = Color(0xFF624D3B) val yellow1 = Color(0xFFFFBC25) val yellow2 = Color(0xFFFFC542) val yellow3 = Color(0xFF625B39) val green1 = Color(0xFF25C685) val green2 = Color(0xFF3DD598) val green3 = Color(0xFF286053) val blue1 = Color(0xFF005DF2) val blue2 = Color(0xFF0062FF) val blue3 = Color(0xFF163E72) val purple1 = Color(0xFF6952DC) val purple2 = Color(0xFF755FE2) val purple3 = Color(0xFF393D69) val gray1 = Color(0xFFFFFFFF) val gray2 = Color(0xFF96A7AF) val gray3 = Color(0xFF475E69) val gray4 = Color(0xFF30444E) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/style/Margins.kt ================================================ package com.prof18.moneyflow.ui.style import androidx.compose.ui.unit.dp internal object Margins { val small = 8.dp val regular = 16.dp val medium = 24.dp val textFieldPadding = 16.dp val horizontalIconPadding = 12.dp val regularCornerRadius = 8.dp val bigCornerRadius = 16.dp } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/style/Shape.kt ================================================ package com.prof18.moneyflow.ui.style import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp internal val MoneyFlowShapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), large = RoundedCornerShape(16.dp), ) ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/style/Theme.kt ================================================ package com.prof18.moneyflow.ui.style import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color private val LightColorScheme = lightColorScheme( primary = LightAppColors.primary, // secondary = LightAppColors.yellow1, background = LightAppColors.background, error = LightAppColors.red1, onPrimary = LightAppColors.lightGrey, onSecondary = DarkAppColors.gray4, onBackground = DarkAppColors.gray4, onSurface = DarkAppColors.gray4, onError = DarkAppColors.gray4, ) private val DarkColorScheme = darkColorScheme( primary = DarkAppColors.primary, // secondary = DarkAppColors.yellow1, error = DarkAppColors.red1, onPrimary = LightAppColors.gray4, onSecondary = LightAppColors.gray4, onBackground = LightAppColors.gray4, onSurface = LightAppColors.gray4, onError = LightAppColors.gray4, ) @Composable internal fun upArrowCircleColor(): Color = if (isSystemInDarkTheme()) DarkAppColors.green3 else LightAppColors.green3 @Composable internal fun upArrowColor(): Color = if (isSystemInDarkTheme()) LightAppColors.green3 else LightAppColors.green1 @Composable internal fun downArrowCircleColor(): Color = if (isSystemInDarkTheme()) DarkAppColors.red3 else LightAppColors.red3 @Composable internal fun downArrowColor(): Color = if (isSystemInDarkTheme()) LightAppColors.red3 else LightAppColors.red1 @Composable internal fun MoneyFlowTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit, ) { MaterialTheme( colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme, typography = moneyFlowTypography(), shapes = MoneyFlowShapes, content = content, ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/ui/style/Typography.kt ================================================ package com.prof18.moneyflow.ui.style import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.poppins_extra_light import money_flow.shared.generated.resources.poppins_light import money_flow.shared.generated.resources.poppins_regular import money_flow.shared.generated.resources.poppins_semibold import org.jetbrains.compose.resources.Font @Composable internal fun moneyFlowTypography(): Typography { val poppins = FontFamily( Font(Res.font.poppins_extra_light, FontWeight.ExtraLight), Font(Res.font.poppins_light, FontWeight.Light), Font(Res.font.poppins_regular, FontWeight.Normal), Font(Res.font.poppins_semibold, FontWeight.SemiBold), ) return Typography( displayLarge = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Light, fontSize = 96.sp, letterSpacing = (-1.5).sp, ), displayMedium = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Light, fontSize = 60.sp, letterSpacing = (-0.5).sp, ), displaySmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 48.sp, letterSpacing = 0.sp, ), headlineLarge = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 34.sp, letterSpacing = 0.25.sp, ), headlineMedium = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 24.sp, letterSpacing = 0.sp, ), headlineSmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 20.sp, letterSpacing = 0.15.sp, ), titleMedium = TextStyle( fontFamily = poppins, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, letterSpacing = 0.15.sp, ), titleSmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Light, fontSize = 16.sp, letterSpacing = 0.1.sp, ), bodyLarge = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 16.sp, letterSpacing = 0.5.sp, ), bodyMedium = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 14.sp, letterSpacing = 0.25.sp, ), labelLarge = TextStyle( fontFamily = poppins, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, letterSpacing = 1.25.sp, ), bodySmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.ExtraLight, fontSize = 12.sp, letterSpacing = 0.4.sp, ), labelSmall = TextStyle( fontFamily = poppins, fontWeight = FontWeight.Normal, fontSize = 10.sp, letterSpacing = 1.5.sp, ), ) } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/CurrencyFormatter.kt ================================================ package com.prof18.moneyflow.utils import com.prof18.moneyflow.domain.entities.CurrencyConfig import kotlin.math.abs // TODO: write tests for all these functions internal fun Long.formatAsCurrency(config: CurrencyConfig): String { val factor = tenFactor(config.decimalPlaces) val absoluteValue = abs(this) val wholePart = absoluteValue / factor val decimalPart = absoluteValue % factor val decimalString = if (config.decimalPlaces == 0) { "" } else { ".${decimalPart.toString().padStart(config.decimalPlaces, '0')}" } val sign = if (this < 0) "-" else "" return "$sign${config.symbol}$wholePart$decimalString" } @Suppress("ReturnCount") internal fun String.toAmountCents(config: CurrencyConfig): Long? { val normalized = trim() if (normalized.isEmpty()) return null val sanitized = normalized.replace(',', '.') val parts = sanitized.split(".") if (parts.size > 2) return null val signMultiplier = if (sanitized.startsWith("-")) -1 else 1 val wholePartText = parts[0].removePrefix("-") val wholePart = wholePartText.toLongOrNull()?.let { abs(it) } ?: return null val decimalText = if (parts.size == 2) parts[1] else "" if (decimalText.length > config.decimalPlaces) return null val decimalValue = if (config.decimalPlaces == 0) { 0L } else { decimalText.padEnd(config.decimalPlaces, '0') .take(config.decimalPlaces) .toLongOrNull() ?: return null } val factor = tenFactor(config.decimalPlaces) return abs(signMultiplier * ((wholePart * factor) + decimalValue)) } @Suppress("MagicNumber") private fun tenFactor(decimalPlaces: Int): Long { var factor = 1L repeat(decimalPlaces) { factor *= 10 } return factor } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/DispatcherProvider.kt ================================================ package com.prof18.moneyflow.utils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers internal class DispatcherProvider { fun default(): CoroutineDispatcher = Dispatchers.Default fun main(): CoroutineDispatcher = Dispatchers.Main fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/LocalAppDensity.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density internal var customAppDensity by mutableStateOf(null) internal object LocalAppDensity { val current: Density @Composable get() = LocalDensity.current @Composable infix fun provides(value: Density?): ProvidedValue<*> { val new = value ?: LocalDensity.current return LocalDensity.provides(new) } } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/LocalAppLocale.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue internal var customAppLocale by mutableStateOf(null) internal expect object LocalAppLocale { val current: String @Composable get @Composable infix fun provides(value: String?): ProvidedValue<*> } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/LocalAppTheme.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue internal var customAppThemeIsDark by mutableStateOf(null) internal expect object LocalAppTheme { val current: Boolean @Composable get @Composable infix fun provides(value: Boolean?): ProvidedValue<*> } ================================================ FILE: shared/src/commonMain/kotlin/com/prof18/moneyflow/utils/Utils.kt ================================================ package com.prof18.moneyflow.utils import co.touchlab.kermit.Logger import com.prof18.moneyflow.domain.entities.MoneyFlowError import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.format import kotlinx.datetime.format.char import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import kotlin.time.Clock import kotlin.time.Instant // TODO: write tests for some of these functions internal data class MonthRange( val startMillis: Long, val endMillis: Long, ) private val dayMonthYearFormatter = LocalDate.Format { day() char('/') monthNumber() char('/') year() } internal fun Long.formatDateDayMonthYear( timeZone: TimeZone = TimeZone.currentSystemDefault(), ): String { val dateTime = toLocalDateTime(timeZone) return dateTime.date.format(dayMonthYearFormatter) } internal fun Long.toMonthRange(timeZone: TimeZone = TimeZone.currentSystemDefault()): MonthRange { val dateTime: LocalDateTime = toLocalDateTime(timeZone) val startDate = LocalDate(dateTime.year, dateTime.month, 1) val startMillis = startDate.atStartOfDayIn(timeZone).toEpochMilliseconds() val endMillis = startDate.plus(DatePeriod(months = 1)) .atStartOfDayIn(timeZone) .toEpochMilliseconds() return MonthRange(startMillis = startMillis, endMillis = endMillis) } internal fun currentMonthRange(timeZone: TimeZone = TimeZone.currentSystemDefault()): MonthRange = Clock.System.now().toEpochMilliseconds().toMonthRange(timeZone) internal fun Long.toLocalDateTime(timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime = Instant.fromEpochMilliseconds(this).toLocalDateTime(timeZone) internal fun Throwable.logError(moneyFlowError: MoneyFlowError, message: String? = null) { val logMessage = buildString { append("Error code: ${moneyFlowError.code}") message?.let { append(" - Details: $message") } } Logger.w(this) { logMessage } } ================================================ FILE: shared/src/commonMain/sqldelight/com/prof18/moneyflow/db/AccountTable.sq ================================================ CREATE TABLE AccountTable ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, currencyCode TEXT NOT NULL, currencySymbol TEXT NOT NULL, currencyDecimalPlaces INTEGER NOT NULL, isDefault INTEGER NOT NULL DEFAULT 0, createdAtMillis INTEGER NOT NULL ); selectDefaultAccount: SELECT * FROM AccountTable WHERE isDefault = 1 LIMIT 1; selectAllAccounts: SELECT * FROM AccountTable ORDER BY isDefault DESC, name ASC; selectAccountById: SELECT * FROM AccountTable WHERE id = :id; insertAccount: INSERT INTO AccountTable (name, currencyCode, currencySymbol, currencyDecimalPlaces, isDefault, createdAtMillis) VALUES (?, ?, ?, ?, ?, ?); updateAccount: UPDATE AccountTable SET name = :name, currencyCode = :currencyCode, currencySymbol = :currencySymbol, currencyDecimalPlaces = :decimalPlaces WHERE id = :id; setDefaultAccount: UPDATE AccountTable SET isDefault = CASE WHEN id = :id THEN 1 ELSE 0 END; ================================================ FILE: shared/src/commonMain/sqldelight/com/prof18/moneyflow/db/CategoryTable.sq ================================================ import com.prof18.moneyflow.database.model.TransactionType; CREATE TABLE CategoryTable ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT AS TransactionType NOT NULL, iconName TEXT NOT NULL, isSystem INTEGER NOT NULL DEFAULT 0, createdAtMillis INTEGER NOT NULL ); CREATE INDEX idx_category_type ON CategoryTable(type); selectAll: SELECT * FROM CategoryTable ORDER BY isSystem DESC, name ASC; selectByType: SELECT * FROM CategoryTable WHERE type = :type ORDER BY isSystem DESC, name ASC; selectById: SELECT * FROM CategoryTable WHERE id = :id; insertCategory: INSERT INTO CategoryTable (name, type, iconName, isSystem, createdAtMillis) VALUES (?, ?, ?, ?, ?); updateCategory: UPDATE CategoryTable SET name = :name, iconName = :iconName WHERE id = :id AND isSystem = 0; countTransactionsForCategory: SELECT COUNT(*) FROM TransactionTable WHERE categoryId = :categoryId; ================================================ FILE: shared/src/commonMain/sqldelight/com/prof18/moneyflow/db/TransactionTable.sq ================================================ import com.prof18.moneyflow.database.model.TransactionType; CREATE TABLE TransactionTable ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, accountId INTEGER NOT NULL DEFAULT 1 REFERENCES AccountTable(id), categoryId INTEGER NOT NULL REFERENCES CategoryTable(id), dateMillis INTEGER NOT NULL, amountCents INTEGER NOT NULL, description TEXT, type TEXT AS TransactionType NOT NULL, createdAtMillis INTEGER NOT NULL ); CREATE INDEX idx_transaction_date ON TransactionTable(dateMillis); CREATE INDEX idx_transaction_account ON TransactionTable(accountId); CREATE INDEX idx_transaction_category ON TransactionTable(categoryId); selectLatestTransactions: SELECT T.id, T.dateMillis, T.amountCents, T.description, T.type, C.name AS categoryName, C.iconName FROM TransactionTable T INNER JOIN CategoryTable AS C ON T.categoryId = C.id WHERE T.accountId = :accountId ORDER BY T.dateMillis DESC LIMIT :limit; selectTransactionsPaginated: SELECT T.id, T.dateMillis, T.amountCents, T.description, T.type, C.name AS categoryName, C.iconName FROM TransactionTable T INNER JOIN CategoryTable AS C ON T.categoryId = C.id WHERE T.accountId = :accountId ORDER BY T.dateMillis DESC LIMIT :pageSize OFFSET :offset; insertTransaction: INSERT INTO TransactionTable (accountId, categoryId, dateMillis, amountCents, description, type, createdAtMillis) VALUES (?, ?, ?, ?, ?, ?, ?); selectTransaction: SELECT * FROM TransactionTable WHERE id = :transactionId; deleteTransaction: DELETE FROM TransactionTable WHERE id = :transactionId; selectMonthlyRecap: SELECT SUM(CASE WHEN type = 'INCOME' THEN amountCents ELSE 0 END) AS incomeCents, SUM(CASE WHEN type = 'OUTCOME' THEN amountCents ELSE 0 END) AS outcomeCents FROM TransactionTable WHERE accountId = :accountId AND dateMillis >= :monthStartMillis AND dateMillis < :monthEndMillis; selectAccountBalance: SELECT COALESCE(SUM(CASE WHEN type = 'INCOME' THEN amountCents ELSE -amountCents END), 0) AS balanceCents FROM TransactionTable WHERE accountId = :accountId; ================================================ FILE: shared/src/commonTest/kotlin/com/prof18/moneyflow/utilities/TestDatabaseHelper.kt ================================================ package com.prof18.moneyflow.utilities import com.prof18.moneyflow.database.DatabaseHelper /** * Init driver for each platform. Should *always* be called to setup test */ internal expect fun createDriver() /** * Close driver for each platform. Should *always* be called to tear down test */ internal expect fun closeDriver() internal expect fun getDatabaseHelper(): DatabaseHelper ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/IosBiometricAuthenticator.kt ================================================ package com.prof18.moneyflow import com.prof18.moneyflow.features.authentication.BiometricAuthenticator import kotlinx.cinterop.ExperimentalForeignApi import platform.LocalAuthentication.LAContext import platform.LocalAuthentication.LAPolicyDeviceOwnerAuthentication import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue @OptIn(ExperimentalForeignApi::class) internal class IosBiometricAuthenticator : BiometricAuthenticator { override fun canAuthenticate(): Boolean { val context = LAContext() return context.canEvaluatePolicy(LAPolicyDeviceOwnerAuthentication, null) } override fun authenticate( onSuccess: () -> Unit, onFailure: () -> Unit, onError: () -> Unit, ) { val context = LAContext() context.evaluatePolicy( policy = LAPolicyDeviceOwnerAuthentication, localizedReason = "Unlock MoneyFlow", reply = { success, error -> dispatch_async(dispatch_get_main_queue()) { when { success -> onSuccess() error != null -> onError() else -> onFailure() } } }, ) } } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/IosBiometricAvailabilityChecker.kt ================================================ package com.prof18.moneyflow import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import kotlinx.cinterop.ExperimentalForeignApi import platform.LocalAuthentication.LAContext import platform.LocalAuthentication.LAPolicyDeviceOwnerAuthenticationWithBiometrics @OptIn(ExperimentalForeignApi::class) internal class IosBiometricAvailabilityChecker : BiometricAvailabilityChecker { override fun isBiometricSupported(): Boolean { val context = LAContext() return context.canEvaluatePolicy(LAPolicyDeviceOwnerAuthenticationWithBiometrics, null) } } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/MainViewController.kt ================================================ package com.prof18.moneyflow import androidx.compose.ui.window.ComposeUIViewController import com.prof18.moneyflow.presentation.MoneyFlowApp @Suppress("FunctionName") public fun MainViewController() = ComposeUIViewController { MoneyFlowApp( biometricAuthenticator = IosBiometricAuthenticator(), ) } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/database/DatabaseDriverFactory.kt ================================================ package com.prof18.moneyflow.database import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.prof18.moneyflow.db.MoneyFlowDB internal fun createDatabaseDriver(useDebugDatabaseName: Boolean = false): SqlDriver { return NativeSqliteDriver( schema = MoneyFlowDB.Schema, name = if (useDebugDatabaseName) { DatabaseHelper.APP_DATABASE_NAME_DEBUG } else { DatabaseHelper.APP_DATABASE_NAME_PROD }, ) } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/di/KoinIos.kt ================================================ package com.prof18.moneyflow.di import com.prof18.moneyflow.IosBiometricAvailabilityChecker import com.prof18.moneyflow.database.createDatabaseDriver import com.prof18.moneyflow.features.settings.BiometricAvailabilityChecker import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings import org.koin.core.KoinApplication import org.koin.core.module.Module import org.koin.dsl.module import platform.Foundation.NSUserDefaults public fun initKoinIos(): KoinApplication = initKoin( additionalModules = emptyList(), ) public fun doInitKoinIos(): KoinApplication = initKoinIos() internal actual val platformModule: Module = module { single { NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) } single { createDatabaseDriver(useDebugDatabaseName = false) } single { IosBiometricAvailabilityChecker() } } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/utils/LocalAppLocale.ios.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.InternalComposeUiApi import platform.Foundation.NSLocale import platform.Foundation.NSUserDefaults import platform.Foundation.preferredLanguages @OptIn(InternalComposeUiApi::class) @Suppress("CompositionLocalAllowlist") internal actual object LocalAppLocale { private const val LANG_KEY = "AppleLanguages" private val default = NSLocale.preferredLanguages.first() as String @Suppress("MemberNameEqualsClassName") private val LocalAppLocale = staticCompositionLocalOf { default } actual val current: String @Composable get() = LocalAppLocale.current @Composable actual infix fun provides(value: String?): ProvidedValue<*> { val new = value ?: default if (value == null) { NSUserDefaults.standardUserDefaults.removeObjectForKey(LANG_KEY) } else { NSUserDefaults.standardUserDefaults.setObject(arrayListOf(new), LANG_KEY) } return LocalAppLocale.provides(new) } } ================================================ FILE: shared/src/iosMain/kotlin/com/prof18/moneyflow/utils/LocalAppTheme.ios.kt ================================================ package com.prof18.moneyflow.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.LocalSystemTheme import androidx.compose.ui.SystemTheme @OptIn(InternalComposeUiApi::class) internal actual object LocalAppTheme { actual val current: Boolean @Composable get() = LocalSystemTheme.current == SystemTheme.Dark @Composable actual infix fun provides(value: Boolean?): ProvidedValue<*> { val new = when (value) { true -> SystemTheme.Dark false -> SystemTheme.Light null -> LocalSystemTheme.current } return LocalSystemTheme.provides(new) } } ================================================ FILE: shared/src/iosTest/kotlin/com/prof18/moneyflow/utilities/DatabaseHelperIosTest.kt ================================================ package com.prof18.moneyflow.utilities import kotlin.test.Test import kotlin.test.assertNotNull internal class DatabaseHelperIosTest { @Test fun shouldInitializeAndCloseDatabaseHelper() { createDriver() try { assertNotNull(getDatabaseHelper()) } finally { closeDriver() } } } ================================================ FILE: shared/src/iosTest/kotlin/com/prof18/moneyflow/utilities/TestUtilsIos.kt ================================================ package com.prof18.moneyflow.utilities import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.prof18.moneyflow.database.DatabaseHelper import com.prof18.moneyflow.db.MoneyFlowDB internal actual fun createDriver() { val nativeDriver = NativeSqliteDriver(MoneyFlowDB.Schema, name = "moneydb.db") databaseHelper = DatabaseHelper(nativeDriver) driver = nativeDriver } internal actual fun closeDriver() { driver?.close() driver = null databaseHelper = null } internal actual fun getDatabaseHelper(): DatabaseHelper = requireNotNull(databaseHelper) private var driver: SqlDriver? = null private var databaseHelper: DatabaseHelper? = null ================================================ FILE: version.properties ================================================ MAJOR=1 MINOR=0 PATCH=0 ================================================ FILE: versioning.gradle.kts ================================================ import java.util.Properties import java.text.SimpleDateFormat import java.util.Date val versionProps = Properties() val versionPropertiesFile = rootProject.file("version.properties") if (versionPropertiesFile.exists()) { versionPropertiesFile.inputStream().use { versionProps.load(it) } } else { throw GradleException("Root project version.properties not found! Please ensure it exists with MAJOR, MINOR, PATCH values.") } val appMajorVersion = versionProps.getProperty("MAJOR").toInt() val appMinorVersion = versionProps.getProperty("MINOR").toInt() val appPatchVersion = versionProps.getProperty("PATCH").toInt() fun getCurrentTimestamp(): String { val sdf = SimpleDateFormat("yyyyMMddHHmm") return sdf.format(Date()) } fun appVersionCode(): Int { val ciBuildNumber = System.getenv("GITHUB_RUN_NUMBER") return if (ciBuildNumber != null) { ciBuildNumber.toInt() + 1000 } else { 1017 // Local build version code } } fun appVersionName(): String { return "${appMajorVersion}.${appMinorVersion}.${appPatchVersion}" } project.extra.set("appVersionCode", ::appVersionCode) project.extra.set("appVersionName", ::appVersionName)