diff --git a/.github/chatmodes/dotnet-maui-blazor.chatmode.md b/.github/chatmodes/dotnet-maui-blazor.chatmode.md index ba1fb68d0..13f65eaeb 100644 --- a/.github/chatmodes/dotnet-maui-blazor.chatmode.md +++ b/.github/chatmodes/dotnet-maui-blazor.chatmode.md @@ -5,16 +5,16 @@ tools: ['edit', 'search', 'runCommands', 'runTasks', 'usages', 'problems', 'chan mcpServers: ["upstash/context7"] --- -Expert in .NET MAUI, Blazor WebAssembly, EF Core, .NET 9. Build modern cross-platform apps with clean architecture. +Expert in .NET MAUI, Blazor WebAssembly, EF Core, .NET 10. Build modern cross-platform apps with clean architecture. -## Project: SSW.Rewards.Mobile (.NET 9) +## Project: SSW.Rewards.Mobile (.NET 10) - **MAUI** (`src/MobileUI/`): iOS/Android, CommunityToolkit.Mvvm, Firebase - **Blazor WASM** (`src/AdminUI/`): MudBlazor, API client - **API** (`src/WebAPI/`): MediatR CQRS, SQL Server, EF Core - **Architecture**: Domain → Application → Infrastructure -## C# 13 & .NET 9 +## C# 14 & .NET 10 - File-scoped namespaces, primary constructors, collection expressions `[1, 2, 3]` - Records for DTOs, pattern matching, nullable types, `var`, `[^1]` indexing diff --git a/.github/chatmodes/gh-actions.chatmode.md b/.github/chatmodes/gh-actions.chatmode.md index e86a2193c..ada6536e5 100644 --- a/.github/chatmodes/gh-actions.chatmode.md +++ b/.github/chatmodes/gh-actions.chatmode.md @@ -20,7 +20,7 @@ You are an expert in GitHub Actions CI/CD workflows with deep knowledge of YAML ## Project Context -This is **SSW.Rewards.Mobile**, a .NET 9.0 solution containing: +This is **SSW.Rewards.Mobile**, a .NET 10.0 solution containing: - **MobileUI**: .NET MAUI mobile app (iOS, Android, MacCatalyst) in `src/MobileUI/` - **AdminUI**: Blazor Server admin portal in `src/AdminUI/` @@ -30,9 +30,9 @@ This is **SSW.Rewards.Mobile**, a .NET 9.0 solution containing: ### Build Characteristics -- **.NET SDK**: 9.0.305 (see `global.json`) +- **.NET SDK**: 10.0.100 (see `global.json`) - **Workloads**: `maui`, `android`, `wasm-tools` required -- **Platform TFMs**: `net9.0-android`, `net9.0-ios`, `net9.0-maccatalyst` for mobile +- **Platform TFMs**: `net10.0-android`, `net10.0-ios`, `net10.0-maccatalyst` for mobile - **Secrets/Config**: `google-services.json` for Android (base64 encoded in vars) - **Signing**: iOS requires certificates/provisioning profiles; Android uses keystore - **Azure**: Deploys to Azure App Service (Web API & Admin Portal) + Notification Hub @@ -80,7 +80,7 @@ All workflows are in `.github/workflows/`: - **Workload installation**: `dotnet workload install maui android wasm-tools` - **NuGet cache clearing**: `dotnet nuget locals all --clear` before restore -- **Multi-targeting**: Build with `-f:net9.0-android` or `-f:net9.0-ios` flags +- **Multi-targeting**: Build with `-f:net10.0-android` or `-f:net10.0-ios` flags - **Linker/Trimming**: Recognize trimming errors and suggest `TrimMode` settings - **AOT compilation**: For iOS (required for App Store) - **Secrets injection**: Decode base64 secrets (e.g., `google-services.json`, signing certs) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b470b91a1..5a52c43d2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,7 @@ This file provides GitHub Copilot with essential context about the SSW.Rewards.M ## Quick Reference -**Project Type**: .NET 9 MAUI Mobile App + Blazor WASM Admin + ASP.NET Core API +**Project Type**: .NET 10 MAUI Mobile App + Blazor WASM Admin + ASP.NET Core API **Architecture**: Clean Architecture with CQRS (MediatR) **Database**: SQL Server with EF Core **Key Technologies**: .NET MAUI, Blazor, MediatR, FluentValidation, Entity Framework Core @@ -44,6 +44,7 @@ var orders = await _context.Orders ``` **Rules**: + 1. ✅ **Always** `.AsNoTracking()` for read-only queries 2. ✅ **Always** `.TagWithContext()` after `.AsNoTracking()` 3. ✅ **Always** `.Select()` to project, **never** `Include`/`ThenInclude` for read-only @@ -94,7 +95,7 @@ public partial class HomeViewModel : ObservableObject - **DTOs**: `{Entity}Dto` or `{Entity}ViewModel` - **PascalCase**: Classes, methods, properties, public fields - **camelCase**: Parameters, local variables -- **_camelCase**: Private fields +- **\_camelCase**: Private fields - **Async suffix**: All async methods ## Common Commands @@ -135,6 +136,7 @@ dotnet run --project src/AdminUI ## Documentation See [`AGENTS.md`](../AGENTS.md) for: + - Complete technology stack details - Full architecture documentation - Detailed coding standards @@ -151,4 +153,4 @@ See [`AGENTS.md`](../AGENTS.md) for: --- -**Last Updated**: November 2025 | **Target Framework**: .NET 9.0 | **MAUI Version**: 9.0.21 +**Last Updated**: December 2025 | **Target Framework**: .NET 10.0 | **MAUI Version**: 10.0.x diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 175674ebd..a3dc7dbd3 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -53,7 +53,7 @@ jobs: - name: Build and sign run: dotnet publish src/MobileUI/MobileUI.csproj ` - -f:net9.0-android -c:Release ` + -f:net10.0-android -c:Release ` /p:ApplicationVersion=${{ inputs.build_number }} ` /p:AndroidKeyStore=True ` /p:AndroidSigningKeyStore="${{ github.workspace }}/sswrewards.keystore" ` @@ -65,4 +65,4 @@ jobs: with: name: artifacts-android-${{ inputs.build_number }} path: | - src/MobileUI/bin/Release/net9.0-android/publish/*.aab + src/MobileUI/bin/Release/net10.0-android/publish/*.aab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 960687b41..142f7a302 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: echo ${{ vars.GOOGLE_SERVICES_JSON }} | base64 --decode > src/MobileUI/Platforms/Android/google-services.json - name: Build and sign Android - run: dotnet build src/MobileUI/MobileUI.csproj -f:net9.0-android --configuration Release + run: dotnet build src/MobileUI/MobileUI.csproj -f:net10.0-android --configuration Release - name: Test run: dotnet test ${{ inputs.project_path }} --no-build --configuration Release --logger "trx;LogFileName=test-results.trx" diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml index c19307239..bbe341857 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-build.yml @@ -53,10 +53,10 @@ jobs: - name: Import Provisioning Profile run: | - echo ${{ secrets.APPLE_PROFILE }} | base64 --decode > SSW_Rewards_iOS_Distribution.mobileprovision - mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles - cp SSW_Rewards_iOS_Distribution.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/ - + echo ${{ secrets.APPLE_PROFILE }} | base64 --decode > SSW_Rewards_iOS_Distribution.mobileprovision + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp SSW_Rewards_iOS_Distribution.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/ + - name: Copy Entitlements.plist run: | if [ "${{ inputs.environment }}" == "prod" ]; then @@ -64,11 +64,11 @@ jobs: fi - name: Build and sign - run: dotnet publish src/MobileUI/MobileUI.csproj -v:diag -f:net9.0-ios -c:Release /p:ArchiveOnBuild=true /p:RuntimeIdentifier=ios-arm64 /p:CodeSignKey="${{ secrets.APPLE_CERT_NAME }}" /p:CodesignProvision="${{ secrets.APPLE_PROFILE_NAME }}" + run: dotnet publish src/MobileUI/MobileUI.csproj -v:diag -f:net10.0-ios -c:Release /p:ArchiveOnBuild=true /p:RuntimeIdentifier=ios-arm64 /p:CodeSignKey="${{ secrets.APPLE_CERT_NAME }}" /p:CodesignProvision="${{ secrets.APPLE_PROFILE_NAME }}" - name: Upload iOS Artifact uses: actions/upload-artifact@v4 with: name: artifacts-ios-${{ inputs.build_number }} - path: | - src/MobileUI/bin/Release/net9.0-ios/ios-arm64/publish/*.ipa + path: | + src/MobileUI/bin/Release/net10.0-ios/ios-arm64/publish/*.ipa diff --git a/AGENTS.md b/AGENTS.md index b0da67d64..9ac8696cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file provides AI agents (GitHub Copilot, Cursor, Windsurf, etc.) with essen - **Mobile App**: .NET MAUI (iOS & Android) - **Admin Portal**: Blazor WebAssembly with MudBlazor -- **Backend API**: ASP.NET Core Web API (.NET 9) +- **Backend API**: ASP.NET Core Web API (.NET 10) - **Database**: SQL Server with EF Core - **Authentication**: SSW.Identity (OIDC/OAuth2) - **Architecture**: Clean Architecture with CQRS (MediatR) @@ -105,10 +105,10 @@ Application/Achievements/ ## 🛠️ Technology Stack -### Backend (.NET 9) +### Backend (.NET 10) -- **Framework**: ASP.NET Core 9.0, Minimal APIs -- **ORM**: Entity Framework Core 9.0 (SQL Server provider) +- **Framework**: ASP.NET Core 10.0, Minimal APIs +- **ORM**: Entity Framework Core 10.0 (SQL Server provider) - **CQRS**: MediatR 12.x - **Validation**: FluentValidation 11.x - **Mapping**: AutoMapper (limited use, prefer manual mapping) @@ -118,14 +118,14 @@ Application/Achievements/ ### Frontend (Blazor WASM) -- **Framework**: Blazor WebAssembly (.NET 9) +- **Framework**: Blazor WebAssembly (.NET 10) - **UI Library**: MudBlazor 6.x - **HTTP**: Custom API client with auth handler - **State**: Scoped services per session ### Mobile (.NET MAUI) -- **Framework**: .NET MAUI (.NET 9), targets `net9.0-ios` and `net9.0-android` +- **Framework**: .NET MAUI (.NET 10), targets `net10.0-ios` and `net10.0-android` - **Architecture**: MVVM with `CommunityToolkit.Mvvm` - **Navigation**: Shell-based navigation - **DI**: Built-in .NET DI container @@ -146,7 +146,7 @@ Application/Achievements/ ## 📋 Coding Standards & Best Practices -### C# 13 & .NET 9 Conventions +### C# 14 & .NET 10 Conventions ```csharp // File-scoped namespaces (always) @@ -373,10 +373,10 @@ dotnet ef migrations add MigrationName --project src/Infrastructure --startup-pr dotnet ef database update --project src/Infrastructure --startup-project src/WebAPI # Mobile (Android) -dotnet build src/MobileUI/MobileUI.csproj -f net9.0-android +dotnet build src/MobileUI/MobileUI.csproj -f net10.0-android # Mobile (iOS) - requires macOS -dotnet build src/MobileUI/MobileUI.csproj -f net9.0-ios +dotnet build src/MobileUI/MobileUI.csproj -f net10.0-ios ``` ### Docker Services @@ -641,4 +641,4 @@ When generating code for this project: --- -**Last Updated**: October 2025 | **Target Framework**: .NET 9.0 | **MAUI Version**: 9.0.21 +**Last Updated**: December 2025 | **Target Framework**: .NET 10.0 | **MAUI Version**: 10.0.x diff --git a/README.md b/README.md index 7bff52669..501aee181 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SSW Rewards Mobile App! -This is a .NET MAUI app with a .NET 8 backend. +This is a .NET MAUI app with a .NET 10 backend. Use this app to scan SSW QR codes, earn SSW Points ⭐, claim rewards and win prizes! @@ -21,7 +21,7 @@ Connect the outside world with SSW at events through awesome rewards! ## Roadmap -- Upgrade to .NET 9 +- ✅ Upgraded to .NET 10 - Push notification support - for prize draws 🥳 - Offline access - Better support for white labelling to allow companies to put their own branding to the app diff --git a/WARP.md b/WARP.md index 9259c8adf..05813a57f 100644 --- a/WARP.md +++ b/WARP.md @@ -4,22 +4,23 @@ This file provides guidance to WARP (warp.dev) when working with code in this re ## Project Overview -SSW Rewards Mobile is a .NET MAUI mobile application with a .NET 9 backend API and Blazor admin portal. The app allows users to scan QR codes at SSW events, earn points, complete quizzes, and redeem rewards. The project follows Clean Architecture principles and uses a CQRS pattern with MediatR. +SSW Rewards Mobile is a .NET MAUI mobile application with a .NET 10 backend API and Blazor admin portal. The app allows users to scan QR codes at SSW events, earn points, complete quizzes, and redeem rewards. The project follows Clean Architecture principles and uses a CQRS pattern with MediatR. ## Key Technologies -- **Backend**: .NET 9.0, ASP.NET Core Web API, Entity Framework Core -- **Mobile App**: .NET MAUI (iOS & Android) targeting net9.0-ios and net9.0-android +- **Backend**: .NET 10.0, ASP.NET Core Web API, Entity Framework Core +- **Mobile App**: .NET MAUI (iOS & Android) targeting net10.0-ios and net10.0-android - **Admin Portal**: Blazor Server - **Database**: SQL Server (with Azurite for blob storage in development) - **Authentication**: SSW.Identity (external service) - **Architecture**: Clean Architecture with CQRS pattern - **Testing**: NUnit, FluentAssertions, Moq -- **SDK Version**: 9.0.305 +- **SDK Version**: 10.0.100 ## Development Setup Commands ### Initial Setup + ```bash # Clone and setup development environment git clone https://github.com/SSWConsulting/SSW.Rewards.Mobile.git @@ -37,6 +38,7 @@ pwsh ./up.ps1 ``` ### Build Commands + ```bash # Build entire solution dotnet build SSW.Rewards.sln @@ -52,6 +54,7 @@ dotnet build SSW.Rewards.sln ``` ### Testing Commands + ```bash # Run all tests dotnet test @@ -70,6 +73,7 @@ dotnet test --filter "ClassName" ``` ### Docker Commands + ```bash # Start all services docker compose --profile all up -d @@ -95,11 +99,12 @@ docker compose build ``` ### Mobile Development + ```bash # For Android (requires dev tunnel for API access) devtunnel host -p 5001 # Update Constants.cs ApiBaseUrl with tunnel address -dotnet build src/MobileUI/MobileUI.csproj -f net9.0-android +dotnet build src/MobileUI/MobileUI.csproj -f net10.0-android # Restore MAUI workloads (if needed) dotnet workload update @@ -107,6 +112,7 @@ dotnet workload restore ``` ### Database Commands + ```bash # Create EF migration dotnet ef migrations add MigrationName --project src/Infrastructure --startup-project src/WebAPI @@ -125,17 +131,20 @@ dotnet ef database drop --project src/Infrastructure --startup-project src/WebAP The solution follows Clean Architecture with clear separation of concerns: 1. **Domain** (`src/Domain/`): Core business entities, value objects, and domain events + - Entities (User, Achievement, Reward, Quiz, etc.) - Common base classes (BaseEntity, BaseAuditableEntity, ValueObject) - Domain events and business rules 2. **Application** (`src/Application/`): Business logic and use cases + - CQRS commands and queries using MediatR - Application services and interfaces - DTOs and view models - Cross-cutting behaviors (validation, logging, performance, authorization) 3. **Infrastructure** (`src/Infrastructure/`): External concerns implementation + - Entity Framework data access - External service integrations - File storage, email services, etc. @@ -156,6 +165,7 @@ The application uses CQRS (Command Query Responsibility Segregation) with Mediat - **Behaviors**: Cross-cutting concerns (validation, logging, authorization) Example structure: + ``` Application/ ├── Users/ @@ -181,6 +191,7 @@ Application/ ## Development Guidelines ### Code Organization + - Follow Clean Architecture layering - Use CQRS for all business operations - Place DTOs in `Shared` project for cross-layer communication @@ -188,24 +199,28 @@ Application/ - Use soft delete patterns for data retention and audit trails ### Naming Conventions + - Commands: `CreateUserCommand`, `UpdateRewardCommand` - Queries: `GetUserQuery`, `GetRewardsListQuery` - Handlers: `CreateUserCommandHandler`, `GetUserQueryHandler` - DTOs: `UserDto`, `RewardListDto` ### Database Context Usage + - Use `IApplicationDbContext` interface in Application layer - Tag queries with `.TagWithContext("MethodName")` for debugging - Use `AsNoTracking()` for read-only queries - Include related entities explicitly with `.Include()` ### Mobile Development Specifics + - Update `Constants.cs` with dev tunnel URL for local API testing - Use dependency injection pattern consistently - Follow MVVM pattern with CommunityToolkit.Mvvm - Handle platform-specific code in Platforms folders ### Testing Guidelines + - Unit tests for domain logic and application handlers - Integration tests for API endpoints - Use FluentAssertions for readable test assertions @@ -213,6 +228,7 @@ Application/ - Test both success and failure scenarios ### Performance Considerations + - Use async/await consistently - Leverage EF Core query optimization - Implement caching where appropriate (CacheKeys.cs) @@ -224,6 +240,7 @@ Application/ ## Common Development Patterns ### Adding a New Feature + 1. Create domain entity if needed (`src/Domain/Entities/`) 2. Add EF configuration (`src/Infrastructure/Persistence/Configurations/`) 3. Create application commands/queries (`src/Application/`) @@ -232,6 +249,7 @@ Application/ 6. Add comprehensive tests ### Adding a New API Endpoint + 1. Create command/query in Application layer 2. Add handler with proper validation 3. Create controller action in WebAPI @@ -239,6 +257,7 @@ Application/ 5. Add integration tests ### Database Schema Changes + 1. Modify entities in Domain layer 2. Add/update EF configurations 3. Create migration: `dotnet ef migrations add MigrationName` @@ -248,16 +267,20 @@ Application/ ## Environment Variables & Secrets ### Required User Secrets (WebAPI) + Add via: `dotnet user-secrets set "Key" "Value" --project src/WebAPI` Get actual values from: **Client Secrets | SSW | SSW.Rewards | Developer Secrets** in Keeper ### Development Certificates + Required for HTTPS in Docker. Created via `up.ps1` script or manually: + - Location: `~/.aspnet/https/WebAPI.pfx` - Password: `ThisPassword` ### HangFire Database + HangFire database (`ssw.rewards.hangfire`) is automatically created by the `up.ps1` script if it doesn't exist. ## Recent Features & Improvements @@ -274,6 +297,7 @@ The project has undergone several major updates recently: ## Troubleshooting ### Common Issues + - **Certificate errors**: Recreate dev certificates with `dotnet dev-certs https --clean` - **Mobile build failures**: Run `dotnet workload restore` - **Database connection issues**: Ensure Docker containers are running @@ -281,6 +305,7 @@ The project has undergone several major updates recently: - **HangFire setup issues**: The `up.ps1` script automatically creates the HangFire database ### Useful Debugging + - API Swagger: https://localhost:5001/swagger/index.html - Admin UI: https://localhost:7137 - Database: Connect to `localhost,1433` with SA user (password: `Rewards.Docker1!`) diff --git a/_docs/Instructions-Compile.md b/_docs/Instructions-Compile.md index 939d4aec3..2929fa441 100644 --- a/_docs/Instructions-Compile.md +++ b/_docs/Instructions-Compile.md @@ -77,11 +77,12 @@ class Local,Docker,External clusterStyle; class MobileApp,DevTunnel,AdminUI,WebAPI,SQLServer,Azurite,SSWIdentity,SSWQuizGPT,NotificationHub service; ``` -## Requirements +## Requirements + **TODO** Find Azurite seed data for the API (Tylah might be blind) -- .NET 9 SDK https://dotnet.microsoft.com/en-us/download/dotnet/9.0 -- IDE - Visual Studio Enterprise Latest // Jetbrains Rider // VS Code +- .NET 10 SDK https://dotnet.microsoft.com/en-us/download/dotnet/10.0 +- IDE - Visual Studio Enterprise Latest // Jetbrains Rider // VS Code - Android SDK setup/ installed w/ Xamarin (https://docs.microsoft.com/en-us/xamarin/android/get-started/installation/android-sdk) - iOS SDK setup/installed w/ Xamarin (https://docs.microsoft.com/en-us/xamarin/ios/get-started/installation/) - [PowerShell Core](https://github.com/PowerShell/PowerShell) @@ -100,10 +101,12 @@ class MobileApp,DevTunnel,AdminUI,WebAPI,SQLServer,Azurite,SSWIdentity,SSWQuizGP - **Preferences** | **Locations** | **Command Line Tools** ## Setting up the Repo for Development + ### To work on the API + Admin UI + 1. Clone this Repo https://github.com/SSWConsulting/SSW.Rewards.Mobile.git -2. Download and install latest .NET 9 SDK -3. Get the Secrets from Keeper +2. Download and install latest .NET 10 SDK +3. Get the Secrets from Keeper 1. **Client Secrets | SSW | SSW.Rewards | Developer Secrets** 2. Add them as .NET User Secrets for `WebAPI.csproj` 4. Create a Developer Certificate with command below @@ -111,12 +114,14 @@ class MobileApp,DevTunnel,AdminUI,WebAPI,SQLServer,Azurite,SSWIdentity,SSWQuizGP 1. Run the script below (You can change change this, but the `docker-compose.yml` should be updated appropriately) **Windows Terminal** + ```bash dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\WebAPI.pfx -p ThisPassword dotnet dev-certs https --trust ``` **Mac** + ```bash dotnet dev-certs https -ep ${HOME}/.aspnet/https/WebAPI.pfx -p ThisPassword dotnet dev-certs https --trust @@ -127,29 +132,37 @@ dotnet dev-certs https --trust 5. Cd into the Repo 6. Run the Docker Containers -* On Windows, open PowerShell and run: - ```bash - ./up.ps1 - ``` -* On macOS or Linux, open a terminal and run: + +- On Windows, open PowerShell and run: + +```bash +./up.ps1 +``` + +- On macOS or Linux, open a terminal and run: + ```bash pwsh ./up.ps1 ``` - -You should now be able to access the AdminUI hosted locally at https://localhost:7137 - + +You should now be able to access the AdminUI hosted locally at https://localhost:7137 + You should now be able to access the WebAPI Swagger docs at https://localhost:5001/swagger/index.html **NOTE:** For active development for WebAPI and AdminUI, we can just start up dependencies for them: + ```bash docker compose --profile tools up -d ``` **Note:** You can run only the WebAPI or AdminUI by running: + ```bash docker compose --profile webapi up -d ``` + OR + ```bash docker compose --profile admin up -d ``` @@ -162,18 +175,22 @@ Currently, we need to restore the DB like for instance from staging or another d ## Mobile UI -Follow Microsoft Learn’s step-by-step guide to get your first .NET MAUI project up and running. It lets you run Mobile UI without issues. https://learn.microsoft.com/en-us/dotnet/maui/get-started/first-app?view=net-maui-8.0&tabs=vsmac&pivots=devices-android +Follow Microsoft Learn’s step-by-step guide to get your first .NET MAUI project up and running. It lets you run Mobile UI without issues. https://learn.microsoft.com/en-us/dotnet/maui/get-started/first-app?view=net-maui-8.0&tabs=vsmac&pivots=devices-android ### To work on the Mobile UI (Android SDK) + 1. Run the Docker containers (Only WebApi required) 2. Start a Dev Tunnel for the API + ```bash devtunnel host -p 5001 ``` + 3. Update the `Constants.cs` `ApiBaseUrl` in the **#if DEBUG** block to use your DevTunnel address 4. Run the MobileUI, targeting your Android Emulator ### To work on the Mobile UI (iOS, MacOS Only) + 1. Complete steps 1-3 above 2. Run the MobileUI, targeting your Android Emulator @@ -194,8 +211,10 @@ dotnet workload restore Apple Developer Program Team ### Setting up your own iPhone for testing + If you want to set up to deploy to your own iPhone, talk to an App Manager, it's hard! :) Or you can give it a go yourself. For that you will be using 2 portals - App Store Connect and Apple Developer Portal: + 1. Talk to a team member who has either Admin or App Manager role in App Store Connect. Give them your Apple ID (you can provide your personal one) and they will send you an invite to the Apple Developer team. If you've never used Apple products before, you will need to [create an Apple ID](https://support.apple.com/en-au/108647). You need to be granted **at least the App Manager role** to be able to perform some of the steps below; otherwise ask someone with the App Manager Role or higher to assist you. 2. On your mac create a Certificate Signing Request (CSR) using Keychain. Go to Keychain Access -> Certificate Assistant -> Request a certificate from a certificate authority. Fill the fields and save generated CSR somewhere. 3. Go to https://developer.apple.com, scroll down and click "Certificates, IDs, & Profiles". @@ -209,10 +228,12 @@ Or you can give it a go yourself. For that you will be using 2 portals - App Sto After that you should be able to deploy and debug the application on your iPhone. ### Creating a production build locally + Currently we have GitHub Actions which build and push the application to InternalTesting and TestFlight. But sometimes it's required to build and sign the application locally. Below is a detailed description how to do that. #### iOS + Apple expects developers to have individual development certificates (up to 2 per developer) and share distribution certificates (up to 3 per team). To be able to build and sign an ipa file you need to have a distribution certificate and provisioning profile associated with it installed on your machine. All the secrets can be found in our password manager (PM). @@ -221,17 +242,17 @@ All the secrets can be found in our password manager (PM). 2. Double click the certificate to install it; use the password from PM 3. Download distribution provisioning profile form the Apple Developer Portal 4. Double click the provisioning profile to install it -5. Go to the MobileUI folder -6. Run `dotnet publish -f net9.0-ios -p:ArchiveOnBuild=true -p:CodesignKey="XXX" -p:CodesignProvision="YYY"`; XXX is the CertificateName (in PM) and YYY is ProfileName (same as in Apple Developer Portal) -7. The ipa file in the _../MobileUI/bin/Release/net9.0-ios/ios-arm64/publish/_ folder is ready to be uploaded to TestFlight +5. Cd into the MobileUI folder +6. Run `dotnet publish -f net10.0-ios -p:ArchiveOnBuild=true -p:CodesignKey="XXX" -p:CodesignProvision="YYY"`; XXX is the CertificateName (in PM) and YYY is ProfileName (same as in Apple Developer Portal) +7. The ipa file in the _../MobileUI/bin/Release/net10.0-ios/ios-arm64/publish/_ folder is ready to be uploaded to TestFlight #### Android + 1. Find keystore info in PM 2. Run `echo | base64 --decode > rewards.keystore` 3. To see the info about keystore run `keytool -list -v -keystore rewards.keystore` -4. Run `dotnet publish -f net9.0-android -p:AndroidKeyStore=true -p:AndroidSigningKeyStore=rewards.keystore -p:AndroidSigningKeyAlias=XXX -p:AndroidSigningKeyPass=YYY -p:AndroidSigningStorePass=YYY`; XXX and YYY can be found in PM -5. The signed aab file in the _../MobileUI/bin/Release/net9.0-android/publish/_ folder is ready to be uploaded to InternalTesting - +4. Run `dotnet publish -f net10.0-android -p:AndroidKeyStore=true -p:AndroidSigningKeyStore=rewards.keystore -p:AndroidSigningKeyAlias=XXX -p:AndroidSigningKeyPass=YYY -p:AndroidSigningStorePass=YYY`; XXX and YYY can be found in PM +5. The signed aab file in the _../MobileUI/bin/Release/net10.0-android/publish/_ folder is ready to be uploaded to InternalTesting [Now you are setup, lets get started on a PBI](Definition-of-Ready.md) diff --git a/global.json b/global.json index 29d3769e1..7e16a437e 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.306", - "workloadVersion": "9.0.306", + "version": "10.0.100", + "workloadVersion": "10.0.100", "rollForward": "latestMinor" } } diff --git a/src/AdminUI/AdminUI.csproj b/src/AdminUI/AdminUI.csproj index c077bfeca..a333dca09 100644 --- a/src/AdminUI/AdminUI.csproj +++ b/src/AdminUI/AdminUI.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable abaa8db5-c195-4a86-bd29-069a802aa8d5 SSW.Rewards.Admin.UI @@ -18,14 +18,14 @@ true - - - - - - + + + + + + - + diff --git a/src/AdminUI/Dockerfile b/src/AdminUI/Dockerfile index c61333a52..cca75e929 100644 --- a/src/AdminUI/Dockerfile +++ b/src/AdminUI/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0.306 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.100 AS build WORKDIR /src COPY ["src/WebAPI/WebAPI.csproj", "src/WebAPI/"] COPY ["src/Application/SSW.Rewards.Application.csproj", "src/Application/"] @@ -8,7 +8,7 @@ COPY ["src/Common/Shared.csproj", "src/Common/"] COPY ["src/Infrastructure/SSW.Rewards.Infrastructure.csproj", "src/Infrastructure/"] RUN dotnet restore "src/AdminUI/AdminUI.csproj" -FROM mcr.microsoft.com/dotnet/sdk:9.0.306 AS development +FROM mcr.microsoft.com/dotnet/sdk:10.0.100 AS development COPY . /src WORKDIR /src/src/AdminUI RUN dotnet workload update diff --git a/src/ApiClient/ApiClient.csproj b/src/ApiClient/ApiClient.csproj index f1381f1bb..fc36391bc 100644 --- a/src/ApiClient/ApiClient.csproj +++ b/src/ApiClient/ApiClient.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/Application/SSW.Rewards.Application.csproj b/src/Application/SSW.Rewards.Application.csproj index 5642770a2..7cbbbffb3 100644 --- a/src/Application/SSW.Rewards.Application.csproj +++ b/src/Application/SSW.Rewards.Application.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Application SSW.Rewards.Application enable @@ -10,16 +10,16 @@ - + - - + + - + - - - + + + diff --git a/src/Common/Shared.csproj b/src/Common/Shared.csproj index 287bb3933..43ab10a49 100644 --- a/src/Common/Shared.csproj +++ b/src/Common/Shared.csproj @@ -1,15 +1,15 @@  - net9.0 + net10.0 enable enable SSW.Rewards.Shared - - + + diff --git a/src/Domain/SSW.Rewards.Domain.csproj b/src/Domain/SSW.Rewards.Domain.csproj index fffdb4847..c804372d8 100644 --- a/src/Domain/SSW.Rewards.Domain.csproj +++ b/src/Domain/SSW.Rewards.Domain.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Domain SSW.Rewards.Domain enable @@ -9,7 +9,7 @@ - + diff --git a/src/Infrastructure/SSW.Rewards.Infrastructure.csproj b/src/Infrastructure/SSW.Rewards.Infrastructure.csproj index 6ad8521ad..e998f5101 100644 --- a/src/Infrastructure/SSW.Rewards.Infrastructure.csproj +++ b/src/Infrastructure/SSW.Rewards.Infrastructure.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Infrastructure SSW.Rewards.Infrastructure enable @@ -12,22 +12,22 @@ - + - - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + diff --git a/src/MobileUI/Features/Flyout/FlyoutHeader.xaml b/src/MobileUI/Features/Flyout/FlyoutHeader.xaml index 30852576a..0001770f9 100644 --- a/src/MobileUI/Features/Flyout/FlyoutHeader.xaml +++ b/src/MobileUI/Features/Flyout/FlyoutHeader.xaml @@ -10,7 +10,8 @@ Padding="20,20" x:DataType="vm:FlyoutHeaderViewModel" RowDefinitions="Auto,Auto,Auto,Auto,Auto" - RowSpacing="20"> + RowSpacing="20" + BackgroundColor="{StaticResource FlyoutBackgroundColour}"> diff --git a/src/MobileUI/MobileUI.csproj b/src/MobileUI/MobileUI.csproj index 8567c1f6d..d1be02694 100644 --- a/src/MobileUI/MobileUI.csproj +++ b/src/MobileUI/MobileUI.csproj @@ -1,7 +1,7 @@  - net9.0-ios;net9.0-android + net10.0-ios;net10.0-android Exe SSW.Rewards.Mobile true @@ -28,12 +28,12 @@ false - + false Automatic iPhone Developer - + false Automatic iPhone Developer @@ -65,25 +65,25 @@ - - + + - - - + + + - + - + - - - + + + @@ -94,7 +94,7 @@ - + @@ -116,13 +116,14 @@ - - - - - - - + + + + + + + + diff --git a/src/SSW.Rewards.Enums/SSW.Rewards.Enums.csproj b/src/SSW.Rewards.Enums/SSW.Rewards.Enums.csproj index 125f4c93b..b76014470 100644 --- a/src/SSW.Rewards.Enums/SSW.Rewards.Enums.csproj +++ b/src/SSW.Rewards.Enums/SSW.Rewards.Enums.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/WebAPI/ConfigureServices.cs b/src/WebAPI/ConfigureServices.cs index 445c1bb82..d36fd3b2c 100644 --- a/src/WebAPI/ConfigureServices.cs +++ b/src/WebAPI/ConfigureServices.cs @@ -1,9 +1,8 @@ using FluentValidation.AspNetCore; -using Microsoft.ApplicationInsights.DependencyCollector; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using SSW.Rewards.Application.Common.Interfaces; using SSW.Rewards.Infrastructure.Persistence; using SSW.Rewards.WebAPI.Authorisation; @@ -84,19 +83,9 @@ public static IServiceCollection AddWebAPIServices(this IServiceCollection servi Type = SecuritySchemeType.ApiKey }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement + options.AddSecurityRequirement(document => new OpenApiSecurityRequirement { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - new string[] {} - } + [new OpenApiSecuritySchemeReference("Bearer", document)] = [] }); }); diff --git a/src/WebAPI/Dockerfile b/src/WebAPI/Dockerfile index 9180139fc..69afcc127 100644 --- a/src/WebAPI/Dockerfile +++ b/src/WebAPI/Dockerfile @@ -1,6 +1,6 @@ # Learn about building .NET container images: # https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md -FROM mcr.microsoft.com/dotnet/sdk:9.0.306 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.100 AS build WORKDIR /src COPY ["src/WebAPI/WebAPI.csproj", "src/WebAPI/"] COPY ["src/Application/SSW.Rewards.Application.csproj", "src/Application/"] @@ -20,7 +20,7 @@ RUN dotnet publish "WebAPI.csproj" --no-restore -o /app /p:UseAppHost=false # final stage/image -FROM mcr.microsoft.com/dotnet/aspnet:9.0 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 EXPOSE 5001 WORKDIR /app COPY --from=build /app . diff --git a/src/WebAPI/WebAPI.csproj b/src/WebAPI/WebAPI.csproj index 3d2945cba..3b12eb172 100644 --- a/src/WebAPI/WebAPI.csproj +++ b/src/WebAPI/WebAPI.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.WebAPI SSW.Rewards.WebAPI enable @@ -21,14 +21,14 @@ - + - - - - + + + + diff --git a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj index 9109e869c..6a5455083 100644 --- a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj +++ b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Application.IntegrationTests SSW.Rewards.Application.IntegrationTests @@ -21,15 +21,15 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Application.UnitTests/Achievements/Commands/ClaimAchievementForUserCommandTests.cs b/tests/Application.UnitTests/Achievements/Commands/ClaimAchievementForUserCommandTests.cs index 99c87c6dd..0cdd283fe 100644 --- a/tests/Application.UnitTests/Achievements/Commands/ClaimAchievementForUserCommandTests.cs +++ b/tests/Application.UnitTests/Achievements/Commands/ClaimAchievementForUserCommandTests.cs @@ -1,6 +1,5 @@ using FluentAssertions; using MediatR; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MockQueryable.Moq; using Moq; @@ -10,7 +9,6 @@ using SSW.Rewards.Application.Common.Interfaces; using SSW.Rewards.Domain.Entities; using SSW.Rewards.Enums; -using SSW.Rewards.Shared.DTOs.Achievements; namespace SSW.Rewards.Application.UnitTests.Achievements.Commands; @@ -51,7 +49,7 @@ public async Task Handle_WithValidAchievement_ShouldClaimSuccessfully() SetupUserQuery(userId); var capturedAchievements = new List(); - var mockUserAchievements = new List().AsQueryable().BuildMockDbSet(); + var mockUserAchievements = new List().BuildMockDbSet(); mockUserAchievements.Setup(x => x.Add(It.IsAny())) .Callback(capturedAchievements.Add); @@ -180,7 +178,7 @@ public async Task Handle_WhenSaveFails_ShouldReturnError() SetupAchievementQuery(achievementId, achievementCode); SetupUserQuery(userId); - var mockUserAchievements = new List().AsQueryable().BuildMockDbSet(); + var mockUserAchievements = new List().BuildMockDbSet(); _contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object); _contextMock.Setup(x => x.SaveChangesAsync(It.IsAny())) @@ -213,7 +211,7 @@ public async Task Handle_OnSuccess_ShouldPublishMilestoneCheckNotification() SetupAchievementQuery(achievementId, achievementCode); SetupUserQuery(userId); - var mockUserAchievements = new List().AsQueryable().BuildMockDbSet(); + var mockUserAchievements = new List().BuildMockDbSet(); _contextMock.Setup(x => x.UserAchievements).Returns(mockUserAchievements.Object); _contextMock.Setup(x => x.SaveChangesAsync(It.IsAny())) .ReturnsAsync(1); @@ -245,7 +243,7 @@ public async Task Handle_WithMultipleDifferentUsers_ShouldClaimForEach() const int achievementId = 20; var capturedAchievements = new List(); - var mockUserAchievements = new List().AsQueryable().BuildMockDbSet(); + var mockUserAchievements = new List().BuildMockDbSet(); mockUserAchievements.Setup(x => x.Add(It.IsAny())) .Callback(capturedAchievements.Add); @@ -280,7 +278,7 @@ private void SetupAchievementQuery(int? achievementId, string code) ? new List { new() { Id = achievementId.Value, Code = code } } : new List(); - var mockDbSet = achievements.AsQueryable().BuildMockDbSet(); + var mockDbSet = achievements.BuildMockDbSet(); _contextMock.Setup(x => x.Achievements).Returns(mockDbSet.Object); } @@ -290,13 +288,13 @@ private void SetupUserQuery(int? userId) ? new List { new() { Id = userId.Value } } : new List(); - var mockDbSet = users.AsQueryable().BuildMockDbSet(); + var mockDbSet = users.BuildMockDbSet(); _contextMock.Setup(x => x.Users).Returns(mockDbSet.Object); } private void SetupUserAchievementsQuery(List userAchievements) { - var mockDbSet = userAchievements.AsQueryable().BuildMockDbSet(); + var mockDbSet = userAchievements.BuildMockDbSet(); _contextMock.Setup(x => x.UserAchievements).Returns(mockDbSet.Object); } } diff --git a/tests/Application.UnitTests/Application.UnitTests.csproj b/tests/Application.UnitTests/Application.UnitTests.csproj index 2bf2aaa38..9764fb536 100644 --- a/tests/Application.UnitTests/Application.UnitTests.csproj +++ b/tests/Application.UnitTests/Application.UnitTests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Application.UnitTests SSW.Rewards.Application.UnitTests @@ -11,15 +11,15 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Application.UnitTests/Quizzes/Commands/SubmitAnswerCommandTests.cs b/tests/Application.UnitTests/Quizzes/Commands/SubmitAnswerCommandTests.cs index 386d189ec..db85b614a 100644 --- a/tests/Application.UnitTests/Quizzes/Commands/SubmitAnswerCommandTests.cs +++ b/tests/Application.UnitTests/Quizzes/Commands/SubmitAnswerCommandTests.cs @@ -1,6 +1,5 @@ using FluentAssertions; using MediatR; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MockQueryable.Moq; using Moq; @@ -244,14 +243,14 @@ public async Task Handle_WithEmptyAnswerText_ShouldStillProcess() private void SetupQuestionQuery(QuizQuestion? question) { var questions = question != null ? new List { question } : new List(); - var mockDbSet = questions.AsQueryable().BuildMockDbSet(); + var mockDbSet = questions.BuildMockDbSet(); _contextMock.Setup(x => x.QuizQuestions).Returns(mockDbSet.Object); } private void SetupAnswerQuery(QuizAnswer? answer) { var answers = answer != null ? new List { answer } : new List(); - var mockDbSet = answers.AsQueryable().BuildMockDbSet(); + var mockDbSet = answers.BuildMockDbSet(); _contextMock.Setup(x => x.QuizAnswers).Returns(mockDbSet.Object); } diff --git a/tests/Application.UnitTests/Quizzes/Commands/SubmitUserQuizCommandTests.cs b/tests/Application.UnitTests/Quizzes/Commands/SubmitUserQuizCommandTests.cs index 2128f2bac..ec96d0d9c 100644 --- a/tests/Application.UnitTests/Quizzes/Commands/SubmitUserQuizCommandTests.cs +++ b/tests/Application.UnitTests/Quizzes/Commands/SubmitUserQuizCommandTests.cs @@ -337,7 +337,7 @@ private SubmitUserQuizCommand CreateCommandWithIncorrectAnswers(int quizId) private void SetupMocksForQuery(Quiz quiz) { - var mockDbSet = new List { quiz }.AsQueryable().BuildMockDbSet(); + var mockDbSet = new List { quiz }.BuildMockDbSet(); _contextMock.Setup(x => x.Quizzes).Returns(mockDbSet.Object); } diff --git a/tests/Application.UnitTests/Quizzes/Queries/GetQuizListForUserTests.cs b/tests/Application.UnitTests/Quizzes/Queries/GetQuizListForUserTests.cs index a6c9ad25b..4927b8720 100644 --- a/tests/Application.UnitTests/Quizzes/Queries/GetQuizListForUserTests.cs +++ b/tests/Application.UnitTests/Quizzes/Queries/GetQuizListForUserTests.cs @@ -1,10 +1,8 @@ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using MockQueryable.Moq; using Moq; using NUnit.Framework; using SSW.Rewards.Application.Common.Interfaces; -using SSW.Rewards.Application.Quizzes.Queries.GetQuizListForUser; using SSW.Rewards.Domain.Entities; using SSW.Rewards.Enums; using GetQuizListForUser = SSW.Rewards.Application.Quizzes.Queries.GetQuizListForUser; @@ -312,13 +310,13 @@ private Quiz CreateQuiz(int id, string title, string description, int points, bo private void SetupQuizzesQuery(List quizzes) { - var mockDbSet = quizzes.AsQueryable().BuildMockDbSet(); + var mockDbSet = quizzes.BuildMockDbSet(); _contextMock.Setup(x => x.Quizzes).Returns(mockDbSet.Object); } private void SetupCompletedQuizzesQuery(List completedQuizzes) { - var mockDbSet = completedQuizzes.AsQueryable().BuildMockDbSet(); + var mockDbSet = completedQuizzes.BuildMockDbSet(); _contextMock.Setup(x => x.CompletedQuizzes).Returns(mockDbSet.Object); } diff --git a/tests/Domain.UnitTests/Domain.UnitTests.csproj b/tests/Domain.UnitTests/Domain.UnitTests.csproj index 46a70d189..2ee8af1a3 100644 --- a/tests/Domain.UnitTests/Domain.UnitTests.csproj +++ b/tests/Domain.UnitTests/Domain.UnitTests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 SSW.Rewards.Domain.UnitTests SSW.Rewards.Domain.UnitTests @@ -11,9 +11,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive