diff --git a/BUILDING.md b/BUILDING.md index 9564d789..ef956195 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,43 +1,135 @@ -# Building Plugin.Firebase +# Building Plugin.Firebase (reproducible setup) -## Prerequisites -- .NET SDK version matching `global.json` -- Android workload (for `net9.0-android`) -- iOS workload + Xcode (for `net9.0-ios`, macOS only) +Goal: avoid “works on my machine” issues around .NET SDKs, workloads/packs, and Xcode. -Install workloads (if needed): +## Supported toolchain matrix +This repo targets `net9.0` + mobile TFMs (`net9.0-ios`, `net9.0-android`). + +**Officially supported (reproducible):** +| .NET SDK | Workloads | Notes | +|---:|---|---| +| 9.0.306 | Workload-set mode + workloads aligned to 9.0.306 | Pinned by `global.json`. Used for CI guidance and docs. | + +**Best-effort / observed OK (not CI-pinned):** +| .NET SDK | Workloads | Notes | +|---:|---|---| +| 10.0.100 | Matching workloads for that SDK | Works locally when `global.json` is not used; we don’t guarantee it stays working. | + +If you want to officially support a new toolchain combination (ex: .NET 10), propose it in a PR with CI updates and a documented matrix change. + +## Quick start (diagnose first, then fix) + +### Step 0 — Run the verifier (no installs) +Run: +```bash +bash scripts/verify-env.sh ``` -dotnet workload install android ios + +If `scripts/verify-env.sh` ends with `STATUS=HEALTHY`, you can build iOS right away: +```bash +dotnet build src/Core/Core.csproj -c Release -f net9.0-ios ``` -## Restore & build +If it prints `STATUS=UNHEALTHY`, it will also print `REASON_1..N` and `NEXT_1..N` with suggested fix commands. + +### Step 1 — Fix only if unhealthy (workloads / packs) +When SDK and workloads are out of sync, the most reliable repair path is: +```bash +dotnet workload config --update-mode workload-set +dotnet workload update --from-previous-sdk --version "$(dotnet --version)" +dotnet workload install ios maccatalyst maui --version "$(dotnet --version)" ``` + +Notes: +- You do **not** need to run `dotnet workload update` on every machine every time. Use it when you changed SDK feature bands, when packs are missing, or when you hit workload-related errors. +- Minimal iOS requirement to build `src/Core/Core.csproj` is Apple workloads/packs (`ios`). `maui` is needed for `sample/Playground`. + +Then clean and rebuild: +```bash +dotnet clean Plugin.Firebase.sln -c Release +dotnet build src/Core/Core.csproj -c Release -f net9.0-ios +``` + +## Restore & build +Restore: +```bash dotnet restore Plugin.Firebase.sln +``` + +Build the full solution (includes sample + integration tests): +```bash dotnet build Plugin.Firebase.sln -c Release ``` -Note: building the full solution includes the sample and integration test apps. -Running those apps requires `GoogleService-Info.plist` and `google-services.json` files (not committed). -If you don’t have local Firebase configs, use the `net9.0` build below to validate the packages. +If you don’t have local Firebase configs, use these builds to validate packages: -### Build without mobile workloads -If you want to validate core code without Android/iOS toolchains: -``` +Build without mobile workloads: +```bash dotnet build src/Auth/Auth.csproj -c Release -f net9.0 ``` -## Tests (integration) -Tests live under `tests/Plugin.Firebase.IntegrationTests` and run on a real device. -You must supply your own Firebase config files (not committed): +Build iOS only: +```bash +dotnet build src/Core/Core.csproj -c Release -f net9.0-ios +``` + +## Tests + +### Unit tests (run anywhere) +```bash +dotnet test tests/Plugin.Firebase.UnitTests/Plugin.Firebase.UnitTests.csproj -c Release +``` + +### Integration tests (device-only) +Integration tests require a real device and local Firebase config files (not committed): - `GoogleService-Info.plist` (iOS) - `google-services.json` (Android) Run: -``` +```bash dotnet test tests/Plugin.Firebase.IntegrationTests/Plugin.Firebase.IntegrationTests.csproj --no-build ``` ## Formatting -``` +```bash dotnet format Plugin.Firebase.sln ``` + +Formatting is required before every PR. + +## Troubleshooting + +### Workloads missing (`NETSDK1147`) +Symptoms: +- “To build this project, the following workloads must be installed: ios/android” + +Fix: +```bash +dotnet workload install ios maccatalyst maui --version "$(dotnet --version)" +``` + +### Packs missing / mismatched (often `CS1705`) +Symptoms: +- `CS1705` mentioning `Microsoft.iOS, Version=...` mismatches + +Fix (repair path): +```bash +dotnet workload config --update-mode workload-set +dotnet workload update --from-previous-sdk --version "$(dotnet --version)" +dotnet workload install ios maccatalyst maui --version "$(dotnet --version)" +``` + +### Xcode / CLT / xcode-select +Symptoms: +- `xcodebuild` not found +- `xcode-select -p` points to an unexpected path + +Fix: +```bash +xcodebuild -version +xcode-select -p +sudo xcode-select -s /Applications/Xcode.app/Contents/Developer +``` + +## Glossary +If you need the deeper explanation of “SDK vs workloads vs manifests vs packs vs TFM”, see `docs/toolchain-glossary.md`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e3d65d3..bd8f0e19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,12 @@ Thanks for contributing! This repo is public OSS. Please keep contributions gene 3) Run formatting and relevant builds. 4) Open a PR with a clear description. +Build/toolchain details (macOS iOS builds): see `BUILDING.md`. + ## Development guidelines - Follow `.editorconfig` rules. - Format code before PRs: `dotnet format Plugin.Firebase.sln` +- Formatting is required before every PR. - Avoid app-specific or proprietary context in code/docs. - Never commit secrets (Firebase configs, signing keys, tokens). - Keep API changes backward-compatible when possible. @@ -51,3 +54,6 @@ dotnet format Plugin.Firebase.sln --include $(git diff --name-only --cached) ## Documentation If you change behavior or APIs, update the relevant docs in `docs/` and/or `README.md`. + +## XML docs +When adding or changing public APIs, prefer adding/updating XML doc comments so the generated `.xml` documentation stays useful. diff --git a/README.md b/README.md index fbc4f54e..a10a8f9f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ This is a wrapper library around the native Android and iOS Firebase SDKs which includes cross-platform APIs for most of the Firebase features. Documentation and the included sample app are MAUI-centric, but the plugin should be usable in any cross-platform .NET9+ project. +## Build / toolchain setup +See `BUILDING.md` for a reproducible macOS setup (SDK/workloads/Xcode), verification commands, and troubleshooting. + ## v4.0 Upgrade Notes - The experience of building projects using this plugin has been improved on Windows / Visual Studio. Issues related to hanging builds caused by XamarinBuildDownload and long path issues affecting iOS NuGet packages have been mitigated, if not commpletely resolved. - MAUI-specific dependencies have been removed. This makes the plugin friendlier to non-MAUI mobile .NET projects. diff --git a/docs/toolchain-glossary.md b/docs/toolchain-glossary.md new file mode 100644 index 00000000..8218580a --- /dev/null +++ b/docs/toolchain-glossary.md @@ -0,0 +1,61 @@ +# Toolchain glossary (SDK vs workloads vs manifests vs packs vs TFM) + +This document explains why “mobile builds” can break when .NET SDK versions, workloads/manifests, packs, and Xcode get out of sync. + +## Glossary + +### .NET SDK version (ex: `9.0.306`, `10.0.100`) +The SDK is the `dotnet` toolchain that runs MSBuild and selects workloads/packs. + +Check: +```bash +dotnet --version +``` + +### Workload version (ex: `9.0.306`, `10.0.102`) +The workload “version” is the bundle of workload components used for a given SDK. + +Check: +```bash +dotnet workload --version +dotnet workload --info +``` + +### Workload manifests (ex: `maui 9.0.111/9.0.100` or `10.0.1/10.0.100`) +A manifest is the metadata that defines what a workload installs (packs, templates, etc.). + +You’ll see manifest versions in: +```bash +dotnet workload --info +``` + +### Packs (Apple packs: `Microsoft.iOS.*` under `/packs`) +Packs are the reference assemblies + runtimes + SDK bits used by the Apple toolchain. + +Common location (Homebrew install): +``` +/usr/local/share/dotnet/packs +``` + +Key Apple packs: +- `Microsoft.iOS.Sdk.*` (targets/toolchain pack) +- `Microsoft.iOS.Ref.*` (reference assemblies used at compile time) +- `Microsoft.iOS.Runtime.*` (runtime bits) + +### TFM (Target Framework Moniker) +This repo targets (examples): +- `net9.0` (no mobile toolchain required) +- `net9.0-ios` (iOS toolchain + packs + Xcode required) +- `net9.0-android` (Android toolchain required) + +TFM with an explicit platform version (example): `net9.0-ios18.0` +- Same base .NET version (`net9.0`) but **pins the iOS target platform version** (API surface / analyzers). +- It does **not** install workloads for you. It only changes compile-time targeting. + +## Relationship (text diagram) +``` +.NET SDK (dotnet) -> selects/uses -> workloads + manifests +workloads + manifests -> install -> packs under /packs +TFM (net9.0-ios...) -> consumes -> packs + Xcode toolchain to compile/link +``` + diff --git a/global.json b/global.json index 07a83ac1..c499af2b 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "9.0.306", "allowPrerelease": false, - "rollForward": "latestMinor" + "rollForward": "latestPatch" } -} \ No newline at end of file +} diff --git a/sample/Playground/App.xaml.cs b/sample/Playground/App.xaml.cs index b1b8d171..66bf7bd1 100644 --- a/sample/Playground/App.xaml.cs +++ b/sample/Playground/App.xaml.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Playground; public partial class App : Application @@ -5,7 +6,10 @@ public partial class App : Application public App() { InitializeComponent(); + } - MainPage = new AppShell(); + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); } } \ No newline at end of file diff --git a/sample/Playground/Common/Services/UserInteraction/UserInteractionServiceBase.cs b/sample/Playground/Common/Services/UserInteraction/UserInteractionServiceBase.cs index 4435fbac..6727d64e 100644 --- a/sample/Playground/Common/Services/UserInteraction/UserInteractionServiceBase.cs +++ b/sample/Playground/Common/Services/UserInteraction/UserInteractionServiceBase.cs @@ -1,4 +1,5 @@ using Playground.Resources; +using System.Linq; namespace Playground.Common.Services.UserInteraction; @@ -148,7 +149,8 @@ protected static async Task SetResultToCancelledAfterDurationAsync(TaskCompletio tcs.TrySetResult(DialogButtonIndex.Cancel); } - private static Page CurrentPage => Application.Current.MainPage; + private static Page CurrentPage => Application.Current?.Windows.FirstOrDefault()?.Page + ?? throw new InvalidOperationException("No active window is available."); } public static class DialogButtonIndex diff --git a/sample/Playground/Platforms/Android/MainActivity.cs b/sample/Playground/Platforms/Android/MainActivity.cs index f0051c29..ab0fb2e1 100644 --- a/sample/Playground/Platforms/Android/MainActivity.cs +++ b/sample/Playground/Platforms/Android/MainActivity.cs @@ -7,6 +7,7 @@ using AndroidX.Core.App; using AndroidX.Core.Content; using Plugin.Firebase.CloudMessaging; +using System.Runtime.Versioning; namespace Playground; @@ -29,18 +30,27 @@ private static void HandleIntent(Intent intent) private void RequestPushNotificationsPermission() { - if(Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu && ContextCompat.CheckSelfPermission(this, Manifest.Permission.PostNotifications) != Permission.Granted) { - ActivityCompat.RequestPermissions(this, new[] { Manifest.Permission.PostNotifications }, 0); ; + if(OperatingSystem.IsAndroidVersionAtLeast(33)) { + RequestPostNotificationsPermission(); + } + } + + [SupportedOSPlatform("android33.0")] + private void RequestPostNotificationsPermission() + { + if(ContextCompat.CheckSelfPermission(this, Manifest.Permission.PostNotifications) != Permission.Granted) { + ActivityCompat.RequestPermissions(this, new[] { Manifest.Permission.PostNotifications }, 0); } } private void CreateNotificationChannelIfNeeded() { - if(Build.VERSION.SdkInt >= BuildVersionCodes.O) { + if(OperatingSystem.IsAndroidVersionAtLeast(26)) { CreateNotificationChannel(); } } + [SupportedOSPlatform("android26.0")] private void CreateNotificationChannel() { var channelId = $"{PackageName}.general"; @@ -62,4 +72,4 @@ protected override void OnNewIntent(Intent intent) base.OnNewIntent(intent); HandleIntent(intent); } -} +} \ No newline at end of file diff --git a/sample/Playground/Platforms/iOS/Services/UserInteraction/UserInteractionService.cs b/sample/Playground/Platforms/iOS/Services/UserInteraction/UserInteractionService.cs index 191a1539..9a6dda50 100644 --- a/sample/Playground/Platforms/iOS/Services/UserInteraction/UserInteractionService.cs +++ b/sample/Playground/Platforms/iOS/Services/UserInteraction/UserInteractionService.cs @@ -1,6 +1,8 @@ +#nullable enable +// using TTGSnackBar; +using System.Linq; using Playground.Common.Services.Scheduler; using Playground.Common.Services.UserInteraction; -// using TTGSnackBar; using UIKit; namespace Playground.Platforms.iOS.Services.UserInteraction @@ -8,36 +10,60 @@ namespace Playground.Platforms.iOS.Services.UserInteraction public sealed class UserInteractionService : UserInteractionServiceBase { public UserInteractionService(ISchedulerService schedulerService) - : base(schedulerService) - { - } + : base(schedulerService) { } protected override Task ShowThreeButtonsDialogAsync(UserInfo userInfo) { var tcs = new TaskCompletionSource(); - var alert = UIAlertController.Create(userInfo.Title, userInfo.Message, UIAlertControllerStyle.Alert); + var alert = UIAlertController.Create( + userInfo.Title, + userInfo.Message, + UIAlertControllerStyle.Alert + ); AddDefaultButtonsToDialog(tcs, alert, userInfo.DefaultButtonTexts); AddCancelButtonToAlertIfNeeded(tcs, alert, userInfo.CancelButtonText); - UIApplication.SharedApplication.KeyWindow?.RootViewController?.PresentViewController(alert, true, null); + RootViewController?.PresentViewController(alert, true, null); return tcs.Task; } - private static void AddDefaultButtonsToDialog(TaskCompletionSource tcs, UIAlertController alert, IList defaultButtonTexts) + private static void AddDefaultButtonsToDialog( + TaskCompletionSource tcs, + UIAlertController alert, + IList defaultButtonTexts + ) { for(var i = 0; i < defaultButtonTexts.Count; i++) { alert.AddAction(CreateDefaultAction(tcs, i, defaultButtonTexts)); } } - private static UIAlertAction CreateDefaultAction(TaskCompletionSource tcs, int index, IList defaultButtonTexts) + private static UIAlertAction CreateDefaultAction( + TaskCompletionSource tcs, + int index, + IList defaultButtonTexts + ) { - return UIAlertAction.Create(defaultButtonTexts[index], UIAlertActionStyle.Default, _ => tcs.TrySetResult(index)); + return UIAlertAction.Create( + defaultButtonTexts[index], + UIAlertActionStyle.Default, + _ => tcs.TrySetResult(index) + ); } - private static void AddCancelButtonToAlertIfNeeded(TaskCompletionSource tcs, UIAlertController alert, string text) + private static void AddCancelButtonToAlertIfNeeded( + TaskCompletionSource tcs, + UIAlertController alert, + string text + ) { if(!string.IsNullOrEmpty(text)) { - alert.AddAction(UIAlertAction.Create(text, UIAlertActionStyle.Cancel, _ => tcs.TrySetResult(DialogButtonIndex.Cancel))); + alert.AddAction( + UIAlertAction.Create( + text, + UIAlertActionStyle.Cancel, + _ => tcs.TrySetResult(DialogButtonIndex.Cancel) + ) + ); } } @@ -70,6 +96,11 @@ private static void AddCancelButtonToAlertIfNeeded(TaskCompletionSource tcs // return x => tcs.TrySetResult(index); // } - private static UIViewController RootViewController => UIApplication.SharedApplication.KeyWindow?.RootViewController; + private static UIViewController? RootViewController => + UIApplication + .SharedApplication.ConnectedScenes.OfType() + .SelectMany(scene => scene.Windows) + .FirstOrDefault(window => window.IsKeyWindow) + ?.RootViewController; } } \ No newline at end of file diff --git a/sample/Playground/Playground.csproj b/sample/Playground/Playground.csproj index a0a8c172..7de2e717 100644 --- a/sample/Playground/Playground.csproj +++ b/sample/Playground/Playground.csproj @@ -58,6 +58,7 @@ Platforms\iOS\Entitlements.plist + false diff --git a/scripts/verify-env.sh b/scripts/verify-env.sh new file mode 100755 index 00000000..35941261 --- /dev/null +++ b/scripts/verify-env.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +fail() { + echo "KO: $*" >&2 + exit 1 +} + +warn() { + echo "WARN: $*" >&2 +} + +reasons=() +next_steps=() + +add_reason() { + reasons+=("$1") +} + +add_next() { + next_steps+=("$1") +} + +echo "Plugin.Firebase - environment verification (macOS)" +echo + +uname_s="$(uname -s)" +if [[ "$uname_s" != "Darwin" ]]; then + warn "This script is intended for macOS. Detected: $uname_s" +fi + +echo "== OS ==" +sw_vers || true +echo "arch: $(uname -m)" +echo + +if ! command -v dotnet >/dev/null; then + add_reason "dotnet not found on PATH" + add_next "Install the .NET SDK required by global.json (see BUILDING.md)" +else + echo "== .NET ==" + sdk_version="$(dotnet --version 2>/dev/null || true)" + + pinned_sdk="" + if [[ -f global.json ]]; then + if command -v python3 >/dev/null; then + pinned_sdk="$(python3 -c 'import json; print(json.load(open("global.json"))["sdk"]["version"])' 2>/dev/null || true)" + else + pinned_sdk="$(sed -n 's/.*\"version\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' global.json 2>/dev/null || true)" + pinned_sdk="${pinned_sdk%%$'\n'*}" + fi + fi + + if [[ -z "$sdk_version" ]]; then + if [[ -n "$pinned_sdk" ]]; then + add_reason "dotnet failed to run with this repo's global.json pinned SDK (${pinned_sdk})" + add_next "Install .NET SDK ${pinned_sdk} (or temporarily move global.json out of the repo)" + else + add_reason "dotnet failed to run" + add_next "Reinstall .NET SDK and retry" + fi + else + echo "dotnet --version: ${sdk_version}" + if [[ -n "$pinned_sdk" && "$sdk_version" != "$pinned_sdk" ]]; then + warn "global.json pins SDK: ${pinned_sdk} (dotnet resolved: ${sdk_version})" + add_reason "SDK mismatch: global.json pins ${pinned_sdk} but dotnet resolved ${sdk_version}" + add_next "Install .NET SDK ${pinned_sdk} (recommended), then rerun this script" + fi + fi + echo +fi + +if command -v dotnet >/dev/null; then + echo "dotnet --info (abridged):" + dotnet --info | sed -n '1,90p' || true + echo + + echo "dotnet workload --version:" + dotnet workload --version || true + echo + + echo "dotnet workload list:" + dotnet workload list || true + echo + + echo "dotnet workload --info (abridged):" + dotnet workload --info | sed -n '1,160p' || true + echo +fi + +echo "== Xcode / CLT ==" +if command -v xcodebuild >/dev/null; then + xcodebuild -version || fail "xcodebuild exists but failed; ensure Xcode is installed and licensed" +else + add_reason "xcodebuild not found (Xcode / Command Line Tools missing)" + add_next "Install Xcode, then install Command Line Tools and re-run: xcodebuild -version" +fi + +if command -v xcode-select >/dev/null; then + xcode_path="$(xcode-select -p 2>/dev/null || true)" + if [[ -n "$xcode_path" ]]; then + echo "xcode-select -p: $xcode_path" + else + add_reason "xcode-select -p returned empty (CLT not installed or not configured)" + add_next "Run: xcode-select -p (then configure with sudo xcode-select -s ... if needed)" + fi +else + add_reason "xcode-select not found (Command Line Tools missing)" + add_next "Install Xcode Command Line Tools (CLT) and re-run: xcode-select -p" +fi +echo + +echo "== iOS packs (Apple workloads) ==" +dotnet_root="" +packs_dir="" + +if command -v dotnet >/dev/null; then + if [[ -n "${DOTNET_ROOT:-}" ]]; then + dotnet_root="$DOTNET_ROOT" + else + # Prefer the SDK Base Path from dotnet --info. This is reliable even when `dotnet` is a shim on PATH. + base_path="$(dotnet --info 2>/dev/null | sed -n 's/^ Base Path:[[:space:]]*//p' || true)" + base_path="${base_path%%$'\n'*}" + if [[ -n "$base_path" && -d "$base_path" ]]; then + dotnet_root="$(cd "$base_path/../.." && pwd)" + else + dotnet_path="$(command -v dotnet)" + dotnet_root="$(cd "$(dirname "$dotnet_path")/.." && pwd)" + fi + fi + packs_dir="${dotnet_root}/packs" +fi + +if [[ ! -d "$packs_dir" ]]; then + add_reason "Could not find dotnet packs directory (expected /packs)" + add_next "Check DOTNET_ROOT or install .NET SDK via the official installer/Homebrew, then re-run this script" +else + echo "packs dir: $packs_dir" + + ios_packs=() + while IFS= read -r line; do + [[ -n "$line" ]] && ios_packs+=("$line") + done < <(ls -1 "$packs_dir" 2>/dev/null | grep -E '^Microsoft\.iOS\.' || true) + + ref_packs=() + while IFS= read -r line; do + [[ -n "$line" ]] && ref_packs+=("$line") + done < <(printf "%s\n" "${ios_packs[@]}" | grep -E '^Microsoft\.iOS\.Ref\.' || true) + + sdk_packs=() + while IFS= read -r line; do + [[ -n "$line" ]] && sdk_packs+=("$line") + done < <(printf "%s\n" "${ios_packs[@]}" | grep -E '^Microsoft\.iOS\.Sdk\.' || true) + + if [[ "${#ios_packs[@]}" -eq 0 ]]; then + add_reason "No Microsoft.iOS.* packs found under /packs" + add_next "Install Apple workloads/packs (see BUILDING.md) then re-run this script" + else + echo "found Microsoft.iOS packs (sample):" + printf " - %s\n" "${ios_packs[@]:0:5}" + echo + fi + + if [[ "${#ref_packs[@]}" -eq 0 ]]; then + add_reason "No Microsoft.iOS.Ref.* packs found (missing reference assemblies for compilation)" + add_next "Repair workloads: dotnet workload install ios maccatalyst maui --version \"$(dotnet --version)\"" + fi + + if [[ "${#sdk_packs[@]}" -eq 0 ]]; then + add_reason "No Microsoft.iOS.Sdk.* packs found (missing Apple SDK targets/toolchain pack)" + add_next "Repair workloads: dotnet workload install ios maccatalyst maui --version \"$(dotnet --version)\"" + fi + + # Repo-specific expectation: target iOS TFM from src/Core/Core.csproj (net9.0-ios today). + ios_tfm="" + if [[ -f src/Core/Core.csproj ]]; then + ios_tfm="$(grep -Eo 'net[0-9]+\.[0-9]+-ios([0-9]+\.[0-9]+)?' src/Core/Core.csproj | sed -n '1p' || true)" + ios_tfm="${ios_tfm%%$'\n'*}" + fi + + if [[ -n "$ios_tfm" ]]; then + echo "repo iOS TFM (from src/Core/Core.csproj): $ios_tfm" + + net_part="${ios_tfm%%-*}" # net9.0 + if [[ "$net_part" =~ ^net([0-9]+)\.([0-9]+)$ ]]; then + net_prefix="net${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + expected_ref_re="^Microsoft\\.iOS\\.Ref\\.${net_prefix//./\\.}_" + expected_sdk_re="^Microsoft\\.iOS\\.Sdk\\.${net_prefix//./\\.}_" + + if ! printf "%s\n" "${ref_packs[@]}" | grep -Eq "$expected_ref_re"; then + add_reason "Missing Microsoft.iOS.Ref.${net_prefix}_* packs required to compile ${ios_tfm}" + add_next "Install/repair Apple workloads for ${net_prefix}: dotnet workload install ios maccatalyst maui --version \"$(dotnet --version)\"" + fi + + if ! printf "%s\n" "${sdk_packs[@]}" | grep -Eq "$expected_sdk_re"; then + add_reason "Missing Microsoft.iOS.Sdk.${net_prefix}_* packs required to compile ${ios_tfm}" + add_next "Install/repair Apple workloads for ${net_prefix}: dotnet workload install ios maccatalyst maui --version \"$(dotnet --version)\"" + fi + fi + echo + fi +fi +echo + +status="HEALTHY" +if [[ "${#reasons[@]}" -gt 0 ]]; then + status="UNHEALTHY" +fi + +echo "== Summary ==" +echo "STATUS=${status}" +if [[ -f global.json ]]; then + if command -v python3 >/dev/null; then + pinned_sdk="$(python3 -c 'import json; print(json.load(open("global.json"))["sdk"]["version"])' 2>/dev/null || true)" + else + pinned_sdk="$(sed -n 's/.*\"version\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' global.json 2>/dev/null || true)" + pinned_sdk="${pinned_sdk%%$'\n'*}" + fi + if [[ -n "$pinned_sdk" ]]; then + echo "PINNED_SDK=${pinned_sdk}" + fi +fi +if command -v dotnet >/dev/null; then + echo "SDK_VERSION=$(dotnet --version 2>/dev/null || true)" + echo "WORKLOAD_VERSION=$(dotnet workload --version 2>/dev/null || true)" +fi +if command -v xcodebuild >/dev/null; then + echo "XCODE=$(xcodebuild -version 2>/dev/null | head -n 1 || true)" +fi +if [[ -n "${dotnet_root:-}" ]]; then + echo "DOTNET_ROOT=${dotnet_root}" +fi +if [[ -n "${packs_dir:-}" ]]; then + echo "PACKS_DIR=${packs_dir}" +fi +if [[ -n "${ios_tfm:-}" ]]; then + echo "IOS_TFM=${ios_tfm}" +fi + +if [[ "$status" == "UNHEALTHY" ]]; then + for i in "${!reasons[@]}"; do + n=$((i + 1)) + echo "REASON_${n}=${reasons[$i]}" + done + for i in "${!next_steps[@]}"; do + n=$((i + 1)) + echo "NEXT_${n}=${next_steps[$i]}" + done + exit 1 +fi + +echo "OK: environment looks ready for iOS builds." diff --git a/src/Analytics/Analytics.csproj b/src/Analytics/Analytics.csproj index 9422750c..1031eff2 100644 --- a/src/Analytics/Analytics.csproj +++ b/src/Analytics/Analytics.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.Analytics - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/Analytics/Platforms/iOS/Extensions/DictionaryExtensions.cs b/src/Analytics/Platforms/iOS/Extensions/DictionaryExtensions.cs index 9ca2640e..31589db5 100644 --- a/src/Analytics/Platforms/iOS/Extensions/DictionaryExtensions.cs +++ b/src/Analytics/Platforms/iOS/Extensions/DictionaryExtensions.cs @@ -2,31 +2,55 @@ namespace Plugin.Firebase.Analytics.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting .NET dictionaries to native iOS NSDictionary types. +/// public static class DictionaryExtensions { - public static NSDictionary ToNSDictionary(this IDictionary dictionary) + /// + /// Converts a generic dictionary to a native iOS NSDictionary. + /// + /// The dictionary to convert. + /// A native NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionary( + this IDictionary dictionary + ) { return ((IDictionary) dictionary).ToNSDictionaryFromNonGeneric(); } - public static NSDictionary ToNSDictionaryFromNonGeneric(this IDictionary dictionary) + /// + /// Converts a non-generic dictionary to a native iOS NSDictionary. + /// + /// The non-generic dictionary to convert. + /// A native NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionaryFromNonGeneric( + this IDictionary dictionary + ) { if(dictionary.Count > 0) { var nsDictionary = new NSMutableDictionary(); foreach(DictionaryEntry entry in dictionary) { - PutIntoNSDictionary(new KeyValuePair(entry.Key.ToString(), entry.Value), ref nsDictionary); + PutIntoNSDictionary( + new KeyValuePair(entry.Key.ToString(), entry.Value), + ref nsDictionary + ); } return NSDictionary.FromObjectsAndKeys( nsDictionary.Values.ToArray(), nsDictionary.Keys.ToArray(), - (nint) nsDictionary.Count); + (nint) nsDictionary.Count + ); } else { return new NSDictionary(); } } - private static void PutIntoNSDictionary(KeyValuePair pair, ref NSMutableDictionary nsDictionary) + private static void PutIntoNSDictionary( + KeyValuePair pair, + ref NSMutableDictionary nsDictionary + ) { switch(pair.Value) { case bool x: @@ -57,8 +81,10 @@ private static void PutIntoNSDictionary(KeyValuePair pair, ref N nsDictionary.Add((NSString) pair.Key, x.ToNSDictionary()); break; case IEnumerable> x: - nsDictionary.Add((NSString) pair.Key, - NSArray.FromObjects(x.Select(d => d.ToNSDictionary()).ToArray())); + nsDictionary.Add( + (NSString) pair.Key, + NSArray.FromObjects(x.Select(d => d.ToNSDictionary()).ToArray()) + ); break; default: if(pair.Value is Enum @enum) { @@ -68,7 +94,9 @@ private static void PutIntoNSDictionary(KeyValuePair pair, ref N nsDictionary.Add((NSString) pair.Key, new NSNull()); break; } else { - throw new ArgumentException($"Couldn't put object of type {pair.Value.GetType()} into NSDictionary"); + throw new ArgumentException( + $"Couldn't put object of type {pair.Value.GetType()} into NSDictionary" + ); } } } diff --git a/src/Analytics/Platforms/iOS/FirebaseAnalyticsImplementation.cs b/src/Analytics/Platforms/iOS/FirebaseAnalyticsImplementation.cs index 0a6352a7..b885195d 100644 --- a/src/Analytics/Platforms/iOS/FirebaseAnalyticsImplementation.cs +++ b/src/Analytics/Platforms/iOS/FirebaseAnalyticsImplementation.cs @@ -1,47 +1,61 @@ -using Plugin.Firebase.Core; using Plugin.Firebase.Analytics.Platforms.iOS.Extensions; +using Plugin.Firebase.Core; using FirebaseAnalytics = Firebase.Analytics.Analytics; namespace Plugin.Firebase.Analytics; +/// +/// iOS implementation of Firebase Analytics that wraps the native Firebase Analytics SDK. +/// public sealed class FirebaseAnalyticsImplementation : DisposableBase, IFirebaseAnalytics { + /// public Task GetAppInstanceIdAsync() { return Task.FromResult(FirebaseAnalytics.AppInstanceId); } + /// public void LogEvent(string eventName, IDictionary parameters) { FirebaseAnalytics.LogEvent(eventName, parameters?.ToNSDictionary()); } - public void LogEvent(string eventName, params (string parameterName, object parameterValue)[] parameters) + /// + public void LogEvent( + string eventName, + params (string parameterName, object parameterValue)[] parameters + ) { LogEvent(eventName, parameters?.ToDictionary(x => x.parameterName, x => x.parameterValue)); } + /// public void SetUserId(string id) { FirebaseAnalytics.SetUserId(id); } + /// public void SetUserProperty(string name, string value) { FirebaseAnalytics.SetUserProperty(value, name); } + /// public void SetSessionTimoutDuration(TimeSpan duration) { FirebaseAnalytics.SetSessionTimeoutInterval(duration.TotalSeconds); } + /// public void ResetAnalyticsData() { FirebaseAnalytics.ResetAnalyticsData(); } + /// public bool IsAnalyticsCollectionEnabled { set => FirebaseAnalytics.SetAnalyticsCollectionEnabled(value); } -} +} \ No newline at end of file diff --git a/src/Analytics/Shared/CrossFirebaseAnalytics.cs b/src/Analytics/Shared/CrossFirebaseAnalytics.cs index 1b9133fe..fa3d93eb 100644 --- a/src/Analytics/Shared/CrossFirebaseAnalytics.cs +++ b/src/Analytics/Shared/CrossFirebaseAnalytics.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.Analytics; +/// +/// Cross-platform entry point for Firebase Analytics. +/// public sealed class CrossFirebaseAnalytics { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseAnalytics CreateInstance() { @@ -34,16 +40,21 @@ public static IFirebaseAnalytics Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/Auth/Auth.csproj b/src/Auth/Auth.csproj index 7ba898a6..382fddc2 100644 --- a/src/Auth/Auth.csproj +++ b/src/Auth/Auth.csproj @@ -4,6 +4,7 @@ net9.0;net9.0-android;net9.0-ios true enable + enable 15.0 @@ -20,9 +21,9 @@ $(DefineConstants); - - Plugin.Firebase.Auth - 4.0.1 + + Plugin.Firebase.Auth + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/Auth/Platforms/Android/AuthTokenResultWrapper.cs b/src/Auth/Platforms/Android/AuthTokenResultWrapper.cs index c001221a..7c47b9a0 100644 --- a/src/Auth/Platforms/Android/AuthTokenResultWrapper.cs +++ b/src/Auth/Platforms/Android/AuthTokenResultWrapper.cs @@ -19,9 +19,11 @@ public T GetClaim(string key) public DateTimeOffset AuthDate => DateTimeOffset.FromUnixTimeSeconds(_wrapped.AuthTimestamp); public IDictionary Claims => _wrapped.Claims.ToDictionary(); - public DateTimeOffset ExpirationDate => DateTimeOffset.FromUnixTimeSeconds(_wrapped.ExpirationTimestamp); - public DateTimeOffset IssuedAtDate => DateTimeOffset.FromUnixTimeSeconds(_wrapped.IssuedAtTimestamp); - public string SignInProvider => _wrapped.SignInProvider; - public string SignInSecondFactor => _wrapped.SignInSecondFactor; - public string Token => _wrapped.Token; + public DateTimeOffset ExpirationDate => + DateTimeOffset.FromUnixTimeSeconds(_wrapped.ExpirationTimestamp); + public DateTimeOffset IssuedAtDate => + DateTimeOffset.FromUnixTimeSeconds(_wrapped.IssuedAtTimestamp); + public string? SignInProvider => _wrapped.SignInProvider; + public string? SignInSecondFactor => _wrapped.SignInSecondFactor; + public string? Token => _wrapped.Token; } \ No newline at end of file diff --git a/src/Auth/Platforms/Android/Email/EmailAuth.cs b/src/Auth/Platforms/Android/Email/EmailAuth.cs index fb46ff77..8213c2c2 100644 --- a/src/Auth/Platforms/Android/Email/EmailAuth.cs +++ b/src/Auth/Platforms/Android/Email/EmailAuth.cs @@ -11,7 +11,10 @@ public Task GetCredentialAsync(string email, string password) public async Task CreateUserAsync(string email, string password) { - var result = await FirebaseAuth.Instance.CreateUserWithEmailAndPasswordAsync(email, password); - return new FirebaseUserWrapper(result.User); + var result = await FirebaseAuth.Instance.CreateUserWithEmailAndPasswordAsync( + email, + password + ); + return new FirebaseUserWrapper(result.User!); } } \ No newline at end of file diff --git a/src/Auth/Platforms/Android/Extensions/FirebaseAuthExtensions.cs b/src/Auth/Platforms/Android/Extensions/FirebaseAuthExtensions.cs index d9d8361d..122bfc93 100644 --- a/src/Auth/Platforms/Android/Extensions/FirebaseAuthExtensions.cs +++ b/src/Auth/Platforms/Android/Extensions/FirebaseAuthExtensions.cs @@ -5,12 +5,18 @@ namespace Plugin.Firebase.Auth.Platforms.Android.Extensions; public static class FirebaseAuthExtensions { - public static FirebaseUserWrapper ToAbstract(this FirebaseUser @this, IAdditionalUserInfo additionalUserInfo = null) + public static FirebaseUserWrapper ToAbstract( + this FirebaseUser @this, + IAdditionalUserInfo? additionalUserInfo = null + ) { return new FirebaseUserWrapper(@this); } - public static ProviderInfo ToAbstract(this IUserInfo @this, IAdditionalUserInfo additionalUserInfo = null) + public static ProviderInfo ToAbstract( + this IUserInfo @this, + IAdditionalUserInfo? additionalUserInfo = null + ) { return new ProviderInfo( @this.Uid, @@ -18,34 +24,47 @@ public static ProviderInfo ToAbstract(this IUserInfo @this, IAdditionalUserInfo @this.DisplayName, @this.Email ?? GetEmailFromAdditionalUserInfo(additionalUserInfo), @this.PhoneNumber, - @this.PhotoUrl?.ToString()); + @this.PhotoUrl?.ToString() + ); } - private static string GetEmailFromAdditionalUserInfo(IAdditionalUserInfo additionalUserInfo) + private static string? GetEmailFromAdditionalUserInfo(IAdditionalUserInfo? additionalUserInfo) { var profile = additionalUserInfo?.Profile; if(profile != null && profile.ContainsKey("email")) { - return profile["email"].ToString(); + return profile["email"]?.ToString(); } return null; } - public static NativeActionCodeSettings ToNative(this ActionCodeSettings @this) + public static NativeActionCodeSettings? ToNative(this ActionCodeSettings @this) { - return NativeActionCodeSettings - .NewBuilder() - .SetUrl(@this.Url) - .SetHandleCodeInApp(@this.HandleCodeInApp) - .SetIOSBundleId(@this.IOSBundleId) - .SetAndroidPackageName(@this.AndroidPackageName, @this.AndroidInstallIfNotAvailable, @this.AndroidMinimumVersion) - .Build(); + var builder = NativeActionCodeSettings.NewBuilder(); + + if(@this.Url is not null) { + builder.SetUrl(@this.Url); + } + + builder.SetHandleCodeInApp(@this.HandleCodeInApp); + builder.SetIOSBundleId(@this.IOSBundleId); + + if(@this.AndroidPackageName is not null) { + builder.SetAndroidPackageName( + @this.AndroidPackageName, + @this.AndroidInstallIfNotAvailable, + @this.AndroidMinimumVersion + ); + } + + return builder.Build(); } public static UserMetadata ToAbstract(this IFirebaseUserMetadata @this) { return new UserMetadata( DateTimeOffset.FromUnixTimeMilliseconds(@this.CreationTimestamp), - DateTimeOffset.FromUnixTimeMilliseconds(@this.LastSignInTimestamp)); + DateTimeOffset.FromUnixTimeMilliseconds(@this.LastSignInTimestamp) + ); } public static IAuthTokenResult ToAbstract(this GetTokenResult @this) diff --git a/src/Auth/Platforms/Android/Extensions/JavaObjectExtensions.cs b/src/Auth/Platforms/Android/Extensions/JavaObjectExtensions.cs index 0b19d31a..af70bc33 100644 --- a/src/Auth/Platforms/Android/Extensions/JavaObjectExtensions.cs +++ b/src/Auth/Platforms/Android/Extensions/JavaObjectExtensions.cs @@ -1,12 +1,12 @@ -using Java.Util; using Android.Runtime; +using Java.Util; using IList = System.Collections.IList; namespace Plugin.Firebase.Auth.Platforms.Android.Extensions; public static class JavaObjectExtensions { - public static object ToObject(this Java.Lang.Object @this, Type targetType = null) + public static object ToObject(this Java.Lang.Object @this, Type? targetType = null) { switch(@this) { case Java.Lang.ICharSequence x: @@ -26,15 +26,17 @@ public static object ToObject(this Java.Lang.Object @this, Type targetType = nul case JavaList x: return x.ToList(targetType?.GenericTypeArguments[0]); default: - throw new ArgumentException($"Could not convert Java.Lang.Object of type {@this.GetType()} to object"); + throw new ArgumentException( + $"Could not convert Java.Lang.Object of type {@this.GetType()} to object" + ); } } - public static Java.Lang.Object ToJavaObject(this object @this) + public static Java.Lang.Object? ToJavaObject(this object @this) { switch(@this) { case string x: - return x; + return new Java.Lang.String(x); case int x: return x; case long x: @@ -74,7 +76,9 @@ public static Java.Lang.Object ToJavaObject(this object @this) return (short) Convert.ToUInt16(@this); } } - throw new ArgumentException($"Could not convert object of type {@this.GetType()} to Java.Lang.Object"); + throw new ArgumentException( + $"Could not convert object of type {@this.GetType()} to Java.Lang.Object" + ); } } } \ No newline at end of file diff --git a/src/Auth/Platforms/Android/Extensions/ListExtensions.cs b/src/Auth/Platforms/Android/Extensions/ListExtensions.cs index 3de8c6d2..76a5a433 100644 --- a/src/Auth/Platforms/Android/Extensions/ListExtensions.cs +++ b/src/Auth/Platforms/Android/Extensions/ListExtensions.cs @@ -5,18 +5,21 @@ namespace Plugin.Firebase.Auth.Platforms.Android.Extensions; public static class ListExtensions { - public static IList ToList(this JavaList @this, Type targetType = null) + public static IList ToList(this JavaList @this, Type? targetType = null) { - var list = targetType == null ? new List() : (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType)); + var list = + targetType == null + ? new List() + : (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType))!; for(var i = 0; i < @this.Size(); i++) { var value = @this[i]; if(value is Java.Lang.Object javaValue) { - list.Add(javaValue.ToObject(targetType)); + list!.Add(javaValue.ToObject(targetType)); } else { - list.Add(value); + list!.Add(value); } } - return list; + return list!; } public static JavaList ToJavaList(this IEnumerable @this) diff --git a/src/Auth/Platforms/Android/FirebaseAuthImplementation.cs b/src/Auth/Platforms/Android/FirebaseAuthImplementation.cs index 08bc3710..4a5dbdd2 100644 --- a/src/Auth/Platforms/Android/FirebaseAuthImplementation.cs +++ b/src/Auth/Platforms/Android/FirebaseAuthImplementation.cs @@ -1,8 +1,8 @@ using Android.Gms.Extensions; using Firebase.Auth; using Plugin.Firebase.Auth.Platforms.Android.Email; -using Plugin.Firebase.Auth.Platforms.Android.PhoneNumber; using Plugin.Firebase.Auth.Platforms.Android.Extensions; +using Plugin.Firebase.Auth.Platforms.Android.PhoneNumber; using Plugin.Firebase.Core; using Plugin.Firebase.Core.Exceptions; using Plugin.Firebase.Core.Platforms.Android; @@ -23,7 +23,7 @@ public FirebaseAuthImplementation() _emailAuth = new EmailAuth(); _phoneNumberAuth = new PhoneNumberAuth(); - // apply the default app language for sending emails + // apply the default app language for sending emails _firebaseAuth.UseAppLanguage(); } @@ -47,10 +47,12 @@ private static CrossFirebaseAuthException GetFirebaseAuthException(Exception ex) public async Task SignInWithCustomTokenAsync(string token) { var authResult = await WrapAsync(_firebaseAuth.SignInWithCustomTokenAsync(token)); - return authResult.User.ToAbstract(authResult.AdditionalUserInfo); + return authResult.User!.ToAbstract(authResult.AdditionalUserInfo); } - public async Task SignInWithPhoneNumberVerificationCodeAsync(string verificationCode) + public async Task SignInWithPhoneNumberVerificationCodeAsync( + string verificationCode + ) { var credential = await _phoneNumberAuth.GetCredentialAsync(verificationCode); return await SignInWithCredentialAsync(credential); @@ -59,16 +61,20 @@ public async Task SignInWithPhoneNumberVerificationCodeAsync(stri private async Task SignInWithCredentialAsync(AuthCredential credential) { var authResult = await WrapAsync(_firebaseAuth.SignInWithCredentialAsync(credential)); - return authResult.User.ToAbstract(authResult.AdditionalUserInfo); + return authResult.User!.ToAbstract(authResult.AdditionalUserInfo); } - public async Task SignInWithEmailAndPasswordAsync(string email, string password, bool createsUserAutomatically = true) + public async Task SignInWithEmailAndPasswordAsync( + string email, + string password, + bool createsUserAutomatically = true + ) { var credential = await _emailAuth.GetCredentialAsync(email, password); try { return await SignInWithCredentialAsync(credential); - } catch(CrossFirebaseAuthException e) when( - e.Reason == FIRAuthError.UserNotFound && createsUserAutomatically) { + } catch(CrossFirebaseAuthException e) + when(e.Reason == FIRAuthError.UserNotFound && createsUserAutomatically) { return await CreateUserAsync(email, password); } } @@ -81,16 +87,18 @@ public async Task CreateUserAsync(string email, string password) public async Task SignInWithEmailLinkAsync(string email, string link) { await WrapAsync(_firebaseAuth.SignInWithEmailLink(email, link)); - return _firebaseAuth.CurrentUser.ToAbstract(); + return _firebaseAuth.CurrentUser!.ToAbstract(); } public async Task SignInAnonymouslyAsync() { var authResult = await WrapAsync(_firebaseAuth.SignInAnonymouslyAsync()); - return authResult.User.ToAbstract(authResult.AdditionalUserInfo); + return authResult.User!.ToAbstract(authResult.AdditionalUserInfo); } - public async Task LinkWithPhoneNumberVerificationCodeAsync(string verificationCode) + public async Task LinkWithPhoneNumberVerificationCodeAsync( + string verificationCode + ) { var credential = await _phoneNumberAuth.GetCredentialAsync(verificationCode); return await LinkWithCredentialAsync(credential); @@ -100,10 +108,12 @@ private async Task LinkWithCredentialAsync(AuthCredential credent { var currentUser = _firebaseAuth.CurrentUser; if(currentUser is null) { - throw new FirebaseException("CurrentUser is null. You need to be logged in to use this feature."); + throw new FirebaseException( + "CurrentUser is null. You need to be logged in to use this feature." + ); } var authResult = await WrapAsync(currentUser.LinkWithCredentialAsync(credential)); - return authResult.User.ToAbstract(authResult.AdditionalUserInfo); + return authResult.User!.ToAbstract(authResult.AdditionalUserInfo); } public async Task LinkWithEmailAndPasswordAsync(string email, string password) @@ -114,7 +124,9 @@ public async Task LinkWithEmailAndPasswordAsync(string email, str public async Task SendSignInLink(string toEmail, CrossActionCodeSettings actionCodeSettings) { - await WrapAsync(_firebaseAuth.SendSignInLinkToEmail(toEmail, actionCodeSettings.ToNative())); + await WrapAsync( + _firebaseAuth.SendSignInLinkToEmail(toEmail, actionCodeSettings.ToNative()!) + ); } public Task SignOutAsync() @@ -140,7 +152,9 @@ public async Task SendPasswordResetEmailAsync() { var currentUser = _firebaseAuth.CurrentUser; if(currentUser is null) { - throw new FirebaseException("CurrentUser is null. You need to be logged in to use this feature."); + throw new FirebaseException( + "CurrentUser is null. You need to be logged in to use this feature." + ); } var email = currentUser.Email; @@ -165,10 +179,12 @@ public IDisposable AddAuthStateListener(Action listener) { var authStateListener = new AuthStateListener(_ => listener.Invoke(this)); _firebaseAuth.AddAuthStateListener(authStateListener); - return new DisposableWithAction(() => _firebaseAuth.RemoveAuthStateListener(authStateListener)); + return new DisposableWithAction(() => + _firebaseAuth.RemoveAuthStateListener(authStateListener) + ); } - public IFirebaseUser CurrentUser => _firebaseAuth.CurrentUser?.ToAbstract(); + public IFirebaseUser? CurrentUser => _firebaseAuth.CurrentUser?.ToAbstract(); private static async Task WrapAsync(Task task) { @@ -197,7 +213,8 @@ private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) } } - private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) where T : Java.Lang.Object + private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) + where T : Java.Lang.Object { try { return await task.AsAsync().ConfigureAwait(false); diff --git a/src/Auth/Platforms/Android/FirebaseUserWrapper.cs b/src/Auth/Platforms/Android/FirebaseUserWrapper.cs index b29002f4..bb674ab9 100644 --- a/src/Auth/Platforms/Android/FirebaseUserWrapper.cs +++ b/src/Auth/Platforms/Android/FirebaseUserWrapper.cs @@ -32,8 +32,11 @@ public Task UpdatePasswordAsync(string password) public Task UpdatePhoneNumberAsync(string verificationId, string smsCode) { - return WrapAsync(_wrapped.UpdatePhoneNumberAsync( - PhoneAuthProvider.GetCredential(verificationId, smsCode))); + return WrapAsync( + _wrapped.UpdatePhoneNumberAsync( + PhoneAuthProvider.GetCredential(verificationId, smsCode) + ) + ); } public Task UpdateProfileAsync(string displayName = "", string photoUrl = "") @@ -43,14 +46,17 @@ public Task UpdateProfileAsync(string displayName = "", string photoUrl = "") builder.SetDisplayName(displayName); } if(photoUrl != "") { - builder.SetPhotoUri(photoUrl == null ? null : Uri.Parse(photoUrl)); + builder.SetPhotoUri(string.IsNullOrEmpty(photoUrl) ? null : Uri.Parse(photoUrl)); } return WrapAsync(_wrapped.UpdateProfileAsync(builder.Build())); } - public Task SendEmailVerificationAsync(ActionCodeSettings actionCodeSettings = null) + public Task SendEmailVerificationAsync(ActionCodeSettings? actionCodeSettings = null) { + // Android binding only exposes a single overload; passing null is valid at runtime +#pragma warning disable CS8604 // Possible null reference argument return WrapAsync(_wrapped.SendEmailVerificationAsync(actionCodeSettings?.ToNative())); +#pragma warning restore CS8604 } public Task UnlinkAsync(string providerId) @@ -96,7 +102,8 @@ private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) } } - private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) where T : Java.Lang.Object + private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) + where T : Java.Lang.Object { try { return await task.AsAsync().ConfigureAwait(false); @@ -106,12 +113,13 @@ private static async Task WrapAsync(global::Android.Gms.Tasks.Task task) w } public string Uid => _wrapped.Uid; - public string DisplayName => _wrapped.DisplayName; - public string Email => _wrapped.Email; - public string PhotoUrl => _wrapped.PhotoUrl?.ToString(); + public string? DisplayName => _wrapped.DisplayName; + public string? Email => _wrapped.Email; + public string? PhotoUrl => _wrapped.PhotoUrl?.ToString(); public string ProviderId => _wrapped.ProviderId; public bool IsEmailVerified => _wrapped.IsEmailVerified; public bool IsAnonymous => _wrapped.IsAnonymous; - public IEnumerable ProviderInfos => _wrapped.ProviderData?.Select(x => x.ToAbstract()); - public UserMetadata Metadata => _wrapped.Metadata?.ToAbstract(); + public IEnumerable? ProviderInfos => + _wrapped.ProviderData?.Select(x => x.ToAbstract()); + public UserMetadata? Metadata => _wrapped.Metadata?.ToAbstract(); } \ No newline at end of file diff --git a/src/Auth/Platforms/Android/PhoneNumber/PhoneNumberAuth.cs b/src/Auth/Platforms/Android/PhoneNumber/PhoneNumberAuth.cs index ed261c83..000e78d9 100644 --- a/src/Auth/Platforms/Android/PhoneNumber/PhoneNumberAuth.cs +++ b/src/Auth/Platforms/Android/PhoneNumber/PhoneNumberAuth.cs @@ -6,15 +6,17 @@ namespace Plugin.Firebase.Auth.Platforms.Android.PhoneNumber; public sealed class PhoneNumberAuth { - private string _verificationId; + private string? _verificationId; public Task VerifyPhoneNumberAsync(Activity activity, string phoneNumber) { - var callbacks = new PhoneVerificationStateChangeCallbacks(onCodeSent: x => _verificationId = x.VerificationId); + var callbacks = new PhoneVerificationStateChangeCallbacks(onCodeSent: x => + _verificationId = x.VerificationId + ); var options = PhoneAuthOptions .NewBuilder() .SetPhoneNumber(phoneNumber) - .SetTimeout(Long.ValueOf(60), TimeUnit.Seconds) + .SetTimeout(Long.ValueOf(60), TimeUnit.Seconds!) .SetActivity(activity) .SetCallbacks(callbacks) .Build(); @@ -25,6 +27,11 @@ public Task VerifyPhoneNumberAsync(Activity activity, string phoneNumber) public Task GetCredentialAsync(string verificationCode) { + if(_verificationId is null) { + throw new InvalidOperationException( + "VerifyPhoneNumberAsync must be called before GetCredentialAsync." + ); + } return Task.FromResult(PhoneAuthProvider.GetCredential(_verificationId, verificationCode)); } } \ No newline at end of file diff --git a/src/Auth/Platforms/Android/PhoneNumber/PhoneVerificationStateChangeCallbacks.cs b/src/Auth/Platforms/Android/PhoneNumber/PhoneVerificationStateChangeCallbacks.cs index 9e825aca..35a38416 100644 --- a/src/Auth/Platforms/Android/PhoneNumber/PhoneVerificationStateChangeCallbacks.cs +++ b/src/Auth/Platforms/Android/PhoneNumber/PhoneVerificationStateChangeCallbacks.cs @@ -3,18 +3,23 @@ namespace Plugin.Firebase.Auth.Platforms.Android.PhoneNumber; -public sealed class PhoneVerificationStateChangeCallbacks : PhoneAuthProvider.OnVerificationStateChangedCallbacks +public sealed class PhoneVerificationStateChangeCallbacks + : PhoneAuthProvider.OnVerificationStateChangedCallbacks { - private readonly Action<(string, PhoneAuthProvider.ForceResendingToken)> _onCodeSent; - private readonly Action _onVerificationCompleted; - private readonly Action _onVerificationFailed; - private readonly Action _onCodeAutoRetrievalTimeOut; + private readonly Action<(string, PhoneAuthProvider.ForceResendingToken)>? _onCodeSent; + private readonly Action? _onVerificationCompleted; + private readonly Action? _onVerificationFailed; + private readonly Action? _onCodeAutoRetrievalTimeOut; public PhoneVerificationStateChangeCallbacks( - Action<(string VerificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken)> onCodeSent = null, - Action onVerificationCompleted = null, - Action onVerificationFailed = null, - Action onCodeAutoRetrievalTimeOut = null) + Action<( + string VerificationId, + PhoneAuthProvider.ForceResendingToken forceResendingToken + )>? onCodeSent = null, + Action? onVerificationCompleted = null, + Action? onVerificationFailed = null, + Action? onCodeAutoRetrievalTimeOut = null + ) { _onCodeSent = onCodeSent; _onVerificationCompleted = onVerificationCompleted; @@ -32,7 +37,10 @@ public override void OnVerificationFailed(FirebaseException exception) _onVerificationFailed?.Invoke(exception); } - public override void OnCodeSent(string verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) + public override void OnCodeSent( + string verificationId, + PhoneAuthProvider.ForceResendingToken forceResendingToken + ) { base.OnCodeSent(verificationId, forceResendingToken); _onCodeSent?.Invoke((verificationId, forceResendingToken)); diff --git a/src/Auth/Platforms/iOS/AuthTokenResultWrapper.cs b/src/Auth/Platforms/iOS/AuthTokenResultWrapper.cs index 7c9dd4ec..5fbb6cc0 100644 --- a/src/Auth/Platforms/iOS/AuthTokenResultWrapper.cs +++ b/src/Auth/Platforms/iOS/AuthTokenResultWrapper.cs @@ -4,25 +4,46 @@ namespace Plugin.Firebase.Auth.Platforms.iOS; +/// +/// Wraps a native iOS Firebase AuthTokenResult for cross-platform access. +/// public sealed class AuthTokenResultWrapper : IAuthTokenResult { private readonly AuthTokenResult _wrapped; + /// + /// Initializes a new instance wrapping the specified native auth token result. + /// + /// The native iOS Firebase AuthTokenResult to wrap. public AuthTokenResultWrapper(AuthTokenResult wrapped) { _wrapped = wrapped; } + /// public T GetClaim(string key) { - return (T) _wrapped.Claims[key].ToObject(typeof(T)); + return (T) _wrapped.Claims[key].ToObject(typeof(T))!; } + /// public DateTimeOffset AuthDate => _wrapped.AuthDate.ToDateTimeOffset(); + + /// public IDictionary Claims => _wrapped.Claims.ToDictionary(); + + /// public DateTimeOffset ExpirationDate => _wrapped.ExpirationDate.ToDateTimeOffset(); + + /// public DateTimeOffset IssuedAtDate => _wrapped.IssuedAtDate.ToDateTimeOffset(); - public string SignInProvider => _wrapped.SignInProvider; - public string SignInSecondFactor => _wrapped.SignInSecondFactor; - public string Token => _wrapped.Token; + + /// + public string? SignInProvider => _wrapped.SignInProvider; + + /// + public string? SignInSecondFactor => _wrapped.SignInSecondFactor; + + /// + public string? Token => _wrapped.Token; } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/Email/EmailAuth.cs b/src/Auth/Platforms/iOS/Email/EmailAuth.cs index 1d2d60cf..31e46edf 100644 --- a/src/Auth/Platforms/iOS/Email/EmailAuth.cs +++ b/src/Auth/Platforms/iOS/Email/EmailAuth.cs @@ -3,17 +3,32 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.Email { + /// + /// Provides email/password authentication functionality for iOS using Firebase Auth. + /// public sealed class EmailAuth { + /// + /// Gets a Firebase credential for email/password authentication. + /// + /// The user's email address. + /// The user's password. + /// A task containing the authentication credential. public Task GetCredentialAsync(string email, string password) { return Task.FromResult(EmailAuthProvider.GetCredentialFromPassword(email, password)); } + /// + /// Creates a new user account with the specified email and password. + /// + /// The email address for the new user. + /// The password for the new user. + /// A task containing the created Firebase user. public async Task CreateUserAsync(string email, string password) { - var result = await FirebaseAuth.DefaultInstance.CreateUserAsync(email, password); - return new FirebaseUserWrapper(result.User); + var result = await FirebaseAuth.DefaultInstance!.CreateUserAsync(email, password); + return new FirebaseUserWrapper(result.User!); } } } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/Extensions/DictionaryExtensions.cs b/src/Auth/Platforms/iOS/Extensions/DictionaryExtensions.cs index 841171fa..3c76b2fe 100644 --- a/src/Auth/Platforms/iOS/Extensions/DictionaryExtensions.cs +++ b/src/Auth/Platforms/iOS/Extensions/DictionaryExtensions.cs @@ -2,22 +2,40 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting native iOS NSDictionary types to .NET dictionaries. +/// public static class DictionaryExtensions { + /// + /// Converts a native NSDictionary to a .NET IDictionary with the specified key and value types. + /// + /// The NSDictionary to convert. + /// The target type for dictionary keys. + /// The target type for dictionary values. + /// A .NET dictionary containing the converted key-value pairs. public static IDictionary ToDictionary(this NSDictionary @this, Type keyType, Type valueType) { - var dict = (IDictionary) Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType)); + var dict = (IDictionary) + Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!; foreach(var pair in @this) { - dict[pair.Key.ToObject(keyType)] = pair.Value.ToObject(valueType); + dict![pair.Key.ToObject(keyType)!] = pair.Value.ToObject(valueType); } - return dict; + return dict!; } - public static IDictionary ToDictionary(this NSDictionary @this) + /// + /// Converts a typed NSDictionary to a .NET dictionary with string keys and object values. + /// + /// The typed NSDictionary to convert. + /// A .NET dictionary containing the converted key-value pairs. + public static IDictionary ToDictionary( + this NSDictionary @this + ) { var dict = new Dictionary(); foreach(var (key, value) in @this) { - dict[key.ToString()] = value.ToObject(); + dict[key.ToString()] = value.ToObject()!; } return dict; } diff --git a/src/Auth/Platforms/iOS/Extensions/FirebaseAuthExtensions.cs b/src/Auth/Platforms/iOS/Extensions/FirebaseAuthExtensions.cs index 380d2ddb..b3b07128 100644 --- a/src/Auth/Platforms/iOS/Extensions/FirebaseAuthExtensions.cs +++ b/src/Auth/Platforms/iOS/Extensions/FirebaseAuthExtensions.cs @@ -5,50 +5,96 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting between native iOS Firebase Auth types and cross-platform types. +/// public static class FirebaseAuthExtensions { - public static FirebaseUserWrapper ToAbstract(this User @this, AdditionalUserInfo additionalUserInfo = null) + /// + /// Converts a native iOS Firebase User to a cross-platform FirebaseUserWrapper. + /// + /// The native iOS Firebase User to convert. + /// Optional additional user info from the authentication result. + /// A cross-platform FirebaseUserWrapper instance. + public static FirebaseUserWrapper ToAbstract( + this User @this, + AdditionalUserInfo? additionalUserInfo = null + ) { return new FirebaseUserWrapper(@this); } - public static ProviderInfo ToAbstract(this IUserInfo @this, AdditionalUserInfo additionalUserInfo = null) + /// + /// Converts a native iOS Firebase IUserInfo to a cross-platform ProviderInfo. + /// + /// The native iOS Firebase user info to convert. + /// Optional additional user info that may contain the email. + /// A cross-platform ProviderInfo instance. + public static ProviderInfo ToAbstract( + this IUserInfo @this, + AdditionalUserInfo? additionalUserInfo = null + ) { - return new ProviderInfo - (@this.Uid, + return new ProviderInfo( + @this.Uid, @this.ProviderId, @this.DisplayName, @this.Email ?? GetEmailFromAdditionalUserInfo(additionalUserInfo), @this.PhoneNumber, - @this.PhotoUrl?.AbsoluteString); + @this.PhotoUrl?.AbsoluteString + ); } - private static string GetEmailFromAdditionalUserInfo(AdditionalUserInfo additionalUserInfo) + private static string? GetEmailFromAdditionalUserInfo(AdditionalUserInfo? additionalUserInfo) { var profile = additionalUserInfo?.Profile; if(profile != null && profile.ContainsKey(new NSString("email"))) { - return profile["email"].ToString(); + return profile["email"]?.ToString(); } return null; } + /// + /// Converts cross-platform ActionCodeSettings to native iOS ActionCodeSettings. + /// + /// The cross-platform ActionCodeSettings to convert. + /// A native iOS ActionCodeSettings instance. public static NativeActionCodeSettings ToNative(this ActionCodeSettings @this) { var settings = new NativeActionCodeSettings(); - settings.Url = new NSUrl(@this.Url); + if(@this.Url is not null) { + settings.Url = new NSUrl(@this.Url); + } settings.HandleCodeInApp = @this.HandleCodeInApp; settings.IOSBundleId = @this.IOSBundleId; - settings.SetAndroidPackageName(@this.AndroidPackageName, @this.AndroidInstallIfNotAvailable, @this.AndroidMinimumVersion); + if(@this.AndroidPackageName is not null) { + settings.SetAndroidPackageName( + @this.AndroidPackageName, + @this.AndroidInstallIfNotAvailable, + @this.AndroidMinimumVersion + ); + } return settings; } + /// + /// Converts native iOS Firebase UserMetadata to cross-platform UserMetadata. + /// + /// The native iOS Firebase user metadata to convert. + /// A cross-platform UserMetadata instance. public static UserMetadata ToAbstract(this NativeUserMetadata @this) { return new UserMetadata( - @this.CreationDate.ToDateTimeOffset(), - @this.LastSignInDate.ToDateTimeOffset()); + @this.CreationDate?.ToDateTimeOffset() ?? DateTimeOffset.MinValue, + @this.LastSignInDate?.ToDateTimeOffset() ?? DateTimeOffset.MinValue + ); } + /// + /// Converts native iOS Firebase AuthTokenResult to a cross-platform IAuthTokenResult. + /// + /// The native iOS Firebase auth token result to convert. + /// A cross-platform IAuthTokenResult wrapper. public static IAuthTokenResult ToAbstract(this AuthTokenResult @this) { return new AuthTokenResultWrapper(@this); diff --git a/src/Auth/Platforms/iOS/Extensions/ListExtensions.cs b/src/Auth/Platforms/iOS/Extensions/ListExtensions.cs index 8367742c..da854faa 100644 --- a/src/Auth/Platforms/iOS/Extensions/ListExtensions.cs +++ b/src/Auth/Platforms/iOS/Extensions/ListExtensions.cs @@ -2,14 +2,23 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting native iOS NSArray types to .NET lists. +/// public static class ListExtensions { + /// + /// Converts a native NSArray to a .NET IList with the specified element type. + /// + /// The NSArray to convert. + /// The target type for list elements. + /// A .NET list containing the converted elements. public static IList ToList(this NSArray @this, Type targetType) { - var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType)); + var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType))!; for(nuint i = 0; i < @this.Count; i++) { - list.Add(@this.GetItem(i).ToObject(targetType)); + list!.Add(@this.GetItem(i).ToObject(targetType)); } - return list; + return list!; } } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/Extensions/NSObjectExtensions.cs b/src/Auth/Platforms/iOS/Extensions/NSObjectExtensions.cs index 37dcc958..13d54eb6 100644 --- a/src/Auth/Platforms/iOS/Extensions/NSObjectExtensions.cs +++ b/src/Auth/Platforms/iOS/Extensions/NSObjectExtensions.cs @@ -2,9 +2,19 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting native iOS NSObject types to .NET objects. +/// public static class NSObjectExtensions { - public static object ToObject(this NSObject @this, Type targetType = null) + /// + /// Converts an NSObject to a .NET object of the specified target type. + /// + /// The NSObject to convert. + /// The optional target type for the conversion. + /// The converted .NET object, or null for NSNull values. + /// Thrown when the NSObject type is not supported for conversion. + public static object? ToObject(this NSObject @this, Type? targetType = null) { switch(@this) { case NSNumber x: @@ -14,15 +24,23 @@ public static object ToObject(this NSObject @this, Type targetType = null) case NSDate x: return x.ToDateTimeOffset(); case NSArray x: - return x.ToList(GetGenericListType(targetType)); + return x.ToList(GetGenericListType(targetType)!); case NSNull: return null; default: - throw new ArgumentException($"Could not convert NSObject of type {@this.GetType()} to object"); + throw new ArgumentException( + $"Could not convert NSObject of type {@this.GetType()} to object" + ); } } - public static object ToObject(this NSNumber @this, Type targetType = null) + /// + /// Converts an NSNumber to a .NET object of the specified target type. + /// + /// The NSNumber to convert. + /// The optional target type for the conversion. Defaults to Int32 if not specified. + /// The converted .NET numeric value, or null if the type is not supported. + public static object? ToObject(this NSNumber @this, Type? targetType = null) { if(targetType == null) { return @this.Int32Value; @@ -58,13 +76,14 @@ public static object ToObject(this NSNumber @this, Type targetType = null) } } - private static Type GetGenericListType(Type targetType) + private static Type? GetGenericListType(Type? targetType) { - var genericType = targetType.GenericTypeArguments?.FirstOrDefault(); + var genericType = targetType?.GenericTypeArguments?.FirstOrDefault(); if(genericType == null) { - throw new ArgumentException($"Couldn't get generic list type of targetType {targetType}. Make sure to use a list IList instead of an array T[] as type in your FirestoreObject."); + throw new ArgumentException( + $"Couldn't get generic list type of targetType {targetType}. Make sure to use a list IList instead of an array T[] as type in your FirestoreObject." + ); } return genericType; } - } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/FirebaseAuthImplementation.cs b/src/Auth/Platforms/iOS/FirebaseAuthImplementation.cs index 8eb37248..8e87ebce 100644 --- a/src/Auth/Platforms/iOS/FirebaseAuthImplementation.cs +++ b/src/Auth/Platforms/iOS/FirebaseAuthImplementation.cs @@ -4,24 +4,30 @@ using Plugin.Firebase.Auth.Platforms.iOS.PhoneNumber; using Plugin.Firebase.Core; using Plugin.Firebase.Core.Exceptions; +using CrossActionCodeSettings = Plugin.Firebase.Auth.ActionCodeSettings; using FirebaseAuth = Firebase.Auth.Auth; using Task = System.Threading.Tasks.Task; -using CrossActionCodeSettings = Plugin.Firebase.Auth.ActionCodeSettings; namespace Plugin.Firebase.Auth; +/// +/// iOS implementation of Firebase Authentication that wraps the native Firebase Auth SDK. +/// public sealed class FirebaseAuthImplementation : DisposableBase, IFirebaseAuth { private readonly FirebaseAuth _firebaseAuth; private readonly EmailAuth _emailAuth; private readonly PhoneNumberAuth _phoneNumberAuth; + /// + /// Initializes a new instance of the Firebase Auth implementation for iOS. + /// + /// Thrown when FirebaseAuth.DefaultInstance is null. public FirebaseAuthImplementation() { - _firebaseAuth = FirebaseAuth.DefaultInstance; - if(_firebaseAuth is null) { - throw new FirebaseException("FirebaseAuth.DefaultInstance is null"); - } + _firebaseAuth = + FirebaseAuth.DefaultInstance + ?? throw new FirebaseException("FirebaseAuth.DefaultInstance is null"); _emailAuth = new EmailAuth(); _phoneNumberAuth = new PhoneNumberAuth(); @@ -29,6 +35,7 @@ public FirebaseAuthImplementation() _firebaseAuth.UseAppLanguage(); } + /// public async Task VerifyPhoneNumberAsync(string phoneNumber) { var viewController = GetViewController(); @@ -38,13 +45,17 @@ public async Task VerifyPhoneNumberAsync(string phoneNumber) private static FirebaseAuthException GetFirebaseAuthException(NSErrorException ex) => Plugin.Firebase.Auth.Platforms.iOS.FirebaseAuthExceptionFactory.Create(ex); + /// public async Task SignInWithCustomTokenAsync(string token) { var user = await WrapAsync(_firebaseAuth.SignInWithCustomTokenAsync(token)); return user.User.ToAbstract(); } - public async Task SignInWithPhoneNumberVerificationCodeAsync(string verificationCode) + /// + public async Task SignInWithPhoneNumberVerificationCodeAsync( + string verificationCode + ) { var credential = await _phoneNumberAuth.GetCredentialAsync(verificationCode); return await SignInWithCredentialAsync(credential); @@ -56,35 +67,46 @@ private async Task SignInWithCredentialAsync(AuthCredential crede return authResult.User.ToAbstract(authResult.AdditionalUserInfo); } - public async Task SignInWithEmailAndPasswordAsync(string email, string password, bool createsUserAutomatically = true) + /// + public async Task SignInWithEmailAndPasswordAsync( + string email, + string password, + bool createsUserAutomatically = true + ) { var credential = await _emailAuth.GetCredentialAsync(email, password); try { return await SignInWithCredentialAsync(credential); - } catch(FirebaseAuthException e) when( - e.Reason == FIRAuthError.UserNotFound && createsUserAutomatically) { + } catch(FirebaseAuthException e) + when(e.Reason == FIRAuthError.UserNotFound && createsUserAutomatically) { return await CreateUserAsync(email, password); } } + /// public async Task CreateUserAsync(string email, string password) { return await WrapAsync(_emailAuth.CreateUserAsync(email, password)); } + /// public async Task SignInWithEmailLinkAsync(string email, string link) { var authResult = await WrapAsync(_firebaseAuth.SignInWithLinkAsync(email, link)); return authResult.User.ToAbstract(authResult.AdditionalUserInfo); } + /// public async Task SignInAnonymouslyAsync() { var authResult = await WrapAsync(_firebaseAuth.SignInAnonymouslyAsync()); return authResult.User.ToAbstract(authResult.AdditionalUserInfo); } - public async Task LinkWithPhoneNumberVerificationCodeAsync(string verificationCode) + /// + public async Task LinkWithPhoneNumberVerificationCodeAsync( + string verificationCode + ) { var credential = await _phoneNumberAuth.GetCredentialAsync(verificationCode); return await LinkWithCredentialAsync(credential); @@ -101,17 +123,20 @@ private async Task LinkWithCredentialAsync(AuthCredential credent return authResult.User.ToAbstract(authResult.AdditionalUserInfo); } + /// public async Task LinkWithEmailAndPasswordAsync(string email, string password) { var credential = await _emailAuth.GetCredentialAsync(email, password); return await LinkWithCredentialAsync(credential); } + /// public async Task SendSignInLink(string toEmail, CrossActionCodeSettings actionCodeSettings) { await WrapAsync(_firebaseAuth.SendSignInLinkAsync(toEmail, actionCodeSettings.ToNative())); } + /// public Task SignOutAsync() { _firebaseAuth.SignOut(out var error); @@ -121,6 +146,7 @@ public Task SignOutAsync() : throw GetFirebaseAuthException(new NSErrorException(error)); } + /// public bool IsSignInWithEmailLink(string link) { try { @@ -130,11 +156,14 @@ public bool IsSignInWithEmailLink(string link) } } + /// public async Task SendPasswordResetEmailAsync() { var currentUser = _firebaseAuth.CurrentUser; if(currentUser is null) { - throw new FirebaseException("CurrentUser is null. You need to be logged in to use this feature."); + throw new FirebaseException( + "CurrentUser is null. You need to be logged in to use this feature." + ); } var email = currentUser.Email; @@ -145,27 +174,35 @@ public async Task SendPasswordResetEmailAsync() await WrapAsync(_firebaseAuth.SendPasswordResetAsync(email)); } + /// public async Task SendPasswordResetEmailAsync(string email) { await WrapAsync(_firebaseAuth.SendPasswordResetAsync(email)); } + /// public void UseEmulator(string host, int port) { _firebaseAuth.UseEmulatorWithHost(host, port); } + /// public IDisposable AddAuthStateListener(Action listener) { var handle = _firebaseAuth.AddAuthStateDidChangeListener((_, _) => listener.Invoke(this)); - return new DisposableWithAction(() => _firebaseAuth.RemoveAuthStateDidChangeListener(handle)); + return new DisposableWithAction(() => + _firebaseAuth.RemoveAuthStateDidChangeListener(handle) + ); } private static UIViewController GetViewController() { - var windowScene = UIApplication.SharedApplication.ConnectedScenes.ToArray() - .FirstOrDefault(static x => x.ActivationState == UISceneActivationState.ForegroundActive) - as UIWindowScene; + var windowScene = + UIApplication + .SharedApplication.ConnectedScenes.ToArray() + .FirstOrDefault(static x => + x.ActivationState == UISceneActivationState.ForegroundActive + ) as UIWindowScene; var window = windowScene?.Windows.FirstOrDefault(static x => x.IsKeyWindow); var rootViewController = window?.RootViewController; @@ -194,5 +231,6 @@ private static async Task WrapAsync(Task task) } } - public IFirebaseUser CurrentUser => _firebaseAuth.CurrentUser?.ToAbstract(); + /// + public IFirebaseUser? CurrentUser => _firebaseAuth.CurrentUser?.ToAbstract(); } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/FirebaseUserWrapper.cs b/src/Auth/Platforms/iOS/FirebaseUserWrapper.cs index 48cf32b0..a64f99d9 100644 --- a/src/Auth/Platforms/iOS/FirebaseUserWrapper.cs +++ b/src/Auth/Platforms/iOS/FirebaseUserWrapper.cs @@ -3,36 +3,51 @@ namespace Plugin.Firebase.Auth.Platforms.iOS; +/// +/// Wraps a native iOS Firebase User for cross-platform access. +/// public sealed class FirebaseUserWrapper : IFirebaseUser { private readonly User _wrapped; + /// + /// Initializes a new instance wrapping the specified native Firebase user. + /// + /// The native iOS Firebase User to wrap. public FirebaseUserWrapper(User firebaseUser) { _wrapped = firebaseUser; } + /// public override string ToString() { return $"[{nameof(FirebaseUserWrapper)}: {nameof(Uid)}={Uid}, {nameof(Email)}={Email}]"; } + /// public Task UpdateEmailAsync(string email) { return WrapAsync(_wrapped.UpdateEmailAsync(email)); } + /// public Task UpdatePasswordAsync(string password) { return WrapAsync(_wrapped.UpdatePasswordAsync(password)); } + /// public Task UpdatePhoneNumberAsync(string verificationId, string smsCode) { - return WrapAsync(_wrapped.UpdatePhoneNumberCredentialAsync( - PhoneAuthProvider.DefaultInstance.GetCredential(verificationId, smsCode))); + return WrapAsync( + _wrapped.UpdatePhoneNumberCredentialAsync( + PhoneAuthProvider.DefaultInstance.GetCredential(verificationId, smsCode) + ) + ); } + /// public Task UpdateProfileAsync(string displayName = "", string photoUrl = "") { var request = _wrapped.ProfileChangeRequest(); @@ -45,24 +60,29 @@ public Task UpdateProfileAsync(string displayName = "", string photoUrl = "") return WrapAsync(request.CommitChangesAsync()); } - public Task SendEmailVerificationAsync(ActionCodeSettings actionCodeSettings = null) + /// + public Task SendEmailVerificationAsync(ActionCodeSettings? actionCodeSettings = null) { return WrapAsync( actionCodeSettings == null ? _wrapped.SendEmailVerificationAsync() - : _wrapped.SendEmailVerificationAsync(actionCodeSettings.ToNative())); + : _wrapped.SendEmailVerificationAsync(actionCodeSettings.ToNative()) + ); } + /// public Task UnlinkAsync(string providerId) { return WrapAsync(_wrapped.UnlinkAsync(providerId)); } + /// public Task DeleteAsync() { return WrapAsync(_wrapped.DeleteAsync()); } + /// public async Task GetIdTokenResultAsync(bool forceRefresh = false) { var result = await WrapAsync(_wrapped.GetIdTokenResultAsync(forceRefresh)); @@ -87,13 +107,31 @@ private static async Task WrapAsync(Task task) } } + /// public string Uid => _wrapped.Uid; - public string DisplayName => _wrapped.DisplayName; - public string Email => _wrapped.Email; - public string PhotoUrl => _wrapped.PhotoUrl?.AbsoluteString; + + /// + public string? DisplayName => _wrapped.DisplayName; + + /// + public string? Email => _wrapped.Email; + + /// + public string? PhotoUrl => _wrapped.PhotoUrl?.AbsoluteString; + + /// public string ProviderId => _wrapped.ProviderId; + + /// public bool IsEmailVerified => _wrapped.IsEmailVerified; + + /// public bool IsAnonymous => _wrapped.IsAnonymous; - public IEnumerable ProviderInfos => _wrapped.ProviderData?.Select(x => x.ToAbstract()); - public UserMetadata Metadata => _wrapped.Metadata?.ToAbstract(); + + /// + public IEnumerable? ProviderInfos => + _wrapped.ProviderData?.Select(x => x.ToAbstract()); + + /// + public UserMetadata? Metadata => _wrapped.Metadata?.ToAbstract(); } \ No newline at end of file diff --git a/src/Auth/Platforms/iOS/PhoneNumber/PhoneNumberAuth.cs b/src/Auth/Platforms/iOS/PhoneNumber/PhoneNumberAuth.cs index e2f0e97d..b934911e 100644 --- a/src/Auth/Platforms/iOS/PhoneNumber/PhoneNumberAuth.cs +++ b/src/Auth/Platforms/iOS/PhoneNumber/PhoneNumberAuth.cs @@ -2,28 +2,69 @@ namespace Plugin.Firebase.Auth.Platforms.iOS.PhoneNumber; +/// +/// Provides phone number authentication functionality for iOS using Firebase Auth. +/// Implements IAuthUIDelegate to handle the reCAPTCHA verification UI. +/// public sealed class PhoneNumberAuth : NSObject, IAuthUIDelegate { - private UIViewController _viewController; - private string _verificationId; + private UIViewController? _viewController; + private string? _verificationId; + /// + /// Initiates phone number verification by sending an SMS code to the specified phone number. + /// + /// The view controller to use for presenting the reCAPTCHA UI. + /// The phone number to verify in E.164 format. + /// A task that completes when the verification SMS has been sent. public async Task VerifyPhoneNumberAsync(UIViewController viewController, string phoneNumber) { _viewController = viewController; - _verificationId = await PhoneAuthProvider.DefaultInstance.VerifyPhoneNumberAsync(phoneNumber, this); + _verificationId = await PhoneAuthProvider.DefaultInstance.VerifyPhoneNumberAsync( + phoneNumber, + this + ); } + /// + /// Gets a Firebase credential using the verification code received via SMS. + /// + /// The SMS verification code entered by the user. + /// A task containing the phone authentication credential. + /// Thrown when VerifyPhoneNumberAsync has not been called first. public Task GetCredentialAsync(string verificationCode) { - return Task.FromResult(PhoneAuthProvider.DefaultInstance.GetCredential(_verificationId, verificationCode)); + if(_verificationId is null) { + throw new InvalidOperationException( + "VerifyPhoneNumberAsync must be called before GetCredentialAsync." + ); + } + return Task.FromResult( + PhoneAuthProvider.DefaultInstance.GetCredential(_verificationId, verificationCode) + ); } - public void PresentViewController(UIViewController viewControllerToPresent, bool animated, Action completion) + /// + /// Presents a view controller for the reCAPTCHA verification flow. + /// + /// The view controller to present. + /// Whether to animate the presentation. + /// Optional completion handler called after presentation. + public void PresentViewController( + UIViewController viewControllerToPresent, + bool animated, + Action? completion + ) { _viewController?.PresentViewController(viewControllerToPresent, animated, completion); } - public void DismissViewController(bool animated, Action completion) + /// + /// Dismisses the currently presented view controller. + /// + /// Whether to animate the dismissal. + /// Optional completion handler called after dismissal. + public void DismissViewController(bool animated, Action? completion) { _viewController?.DismissViewController(animated, completion); } diff --git a/src/Auth/Shared/ActionCodeSettings.cs b/src/Auth/Shared/ActionCodeSettings.cs index ab73fea4..bbef8ba8 100644 --- a/src/Auth/Shared/ActionCodeSettings.cs +++ b/src/Auth/Shared/ActionCodeSettings.cs @@ -1,18 +1,54 @@ namespace Plugin.Firebase.Auth; +/// +/// Settings for configuring action code operations such as password reset, email verification, and email link sign-in. +/// public sealed class ActionCodeSettings { - public void SetAndroidPackageName(string packageName, bool installIfNotAvailable, string minimumVersion) + /// + /// Sets the Android package name for the action code. + /// + /// The Android package name of the app. + /// Whether to install the Android app if not available. + /// The minimum Android app version required. + public void SetAndroidPackageName( + string packageName, + bool installIfNotAvailable, + string minimumVersion + ) { AndroidPackageName = packageName; AndroidInstallIfNotAvailable = installIfNotAvailable; AndroidMinimumVersion = minimumVersion; } - public string Url { get; set; } - public string IOSBundleId { get; set; } - public string AndroidPackageName { get; private set; } - public string AndroidMinimumVersion { get; private set; } + /// + /// Gets or sets the URL to continue to after the action code is handled. + /// + public string? Url { get; set; } + + /// + /// Gets or sets the iOS bundle ID associated with the app. + /// + public string? IOSBundleId { get; set; } + + /// + /// Gets the Android package name of the app. + /// + public string? AndroidPackageName { get; private set; } + + /// + /// Gets the minimum app version required. + /// + public string? AndroidMinimumVersion { get; private set; } + + /// + /// Gets whether to install the Android app if not available. + /// public bool AndroidInstallIfNotAvailable { get; private set; } + + /// + /// Gets or sets whether the action code link will open in a mobile app or web browser. + /// public bool HandleCodeInApp { get; set; } } \ No newline at end of file diff --git a/src/Auth/Shared/CrossFirebaseAuth.cs b/src/Auth/Shared/CrossFirebaseAuth.cs index 7943bf2c..dcf0819a 100644 --- a/src/Auth/Shared/CrossFirebaseAuth.cs +++ b/src/Auth/Shared/CrossFirebaseAuth.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.Auth; +/// +/// Cross-platform entry point for Firebase Authentication. +/// public sealed class CrossFirebaseAuth { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseAuth CreateInstance() { @@ -10,7 +16,7 @@ private static IFirebaseAuth CreateInstance() return new FirebaseAuthImplementation(); #else #pragma warning disable IDE0022 // Use expression body for methods - return null; + return null!; #pragma warning restore IDE0022 // Use expression body for methods #endif } @@ -34,16 +40,21 @@ public static IFirebaseAuth Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/Auth/Shared/IAuthTokenResult.cs b/src/Auth/Shared/IAuthTokenResult.cs index 284961a4..fe80f66d 100644 --- a/src/Auth/Shared/IAuthTokenResult.cs +++ b/src/Auth/Shared/IAuthTokenResult.cs @@ -18,7 +18,6 @@ public interface IAuthTokenResult /// DateTimeOffset AuthDate { get; } - /// /// Stores the entire payload of claims found on the ID token. This includes the standard reserved claims as well as custom claims /// set by the developer via the Admin SDK. @@ -38,15 +37,15 @@ public interface IAuthTokenResult /// /// Stores sign-in provider through which the token was obtained. This does not necessarily map to provider IDs. /// - string SignInProvider { get; } + string? SignInProvider { get; } /// /// Stores sign-in second factor through which the token was obtained. /// - string SignInSecondFactor { get; } + string? SignInSecondFactor { get; } /// /// Stores the JWT string of the ID token. /// - string Token { get; } + string? Token { get; } } \ No newline at end of file diff --git a/src/Auth/Shared/IFirebaseAuth.cs b/src/Auth/Shared/IFirebaseAuth.cs index 1b680e71..0d0286d2 100644 --- a/src/Auth/Shared/IFirebaseAuth.cs +++ b/src/Auth/Shared/IFirebaseAuth.cs @@ -40,7 +40,11 @@ public interface IFirebaseAuth : IDisposable /// The user’s password. /// If the user doesn't exist, it will be created automatically and signed in afterwards if the value is true. /// The signed in IFirebaseUser object. - Task SignInWithEmailAndPasswordAsync(string email, string password, bool createsUserAutomatically = true); + Task SignInWithEmailAndPasswordAsync( + string email, + string password, + bool createsUserAutomatically = true + ); /// /// Signs in using an email address and email sign-in link. @@ -108,7 +112,7 @@ public interface IFirebaseAuth : IDisposable /// The emulator host (for example, 10.0.2.2 on android and localhost on iOS) /// The emulator port (for example, 9099) void UseEmulator(string host, int port); - + /// /// Registers a listener for authentication state changes. /// @@ -119,5 +123,5 @@ public interface IFirebaseAuth : IDisposable /// /// The currently signed in IFirebaseUser object or null if the user is signed out. /// - IFirebaseUser CurrentUser { get; } -} + IFirebaseUser? CurrentUser { get; } +} \ No newline at end of file diff --git a/src/Auth/Shared/IFirebaseUser.cs b/src/Auth/Shared/IFirebaseUser.cs index 972f5abd..66a62e6d 100644 --- a/src/Auth/Shared/IFirebaseUser.cs +++ b/src/Auth/Shared/IFirebaseUser.cs @@ -38,7 +38,7 @@ public interface IFirebaseUser /// Initiates email verification for the user. /// /// An ActionCodeSettings object containing settings related to handling action codes. - Task SendEmailVerificationAsync(ActionCodeSettings actionCodeSettings = null); + Task SendEmailVerificationAsync(ActionCodeSettings? actionCodeSettings = null); /// /// Disassociates a user account from a third-party identity provider with this user. @@ -61,24 +61,24 @@ public interface IFirebaseUser Task GetIdTokenResultAsync(bool forceRefresh = false); /// - /// Returns a string used to uniquely identify your user in your Firebase project's user database. + /// Returns a string used to uniquely identify your user in your Firebase project's user database. /// string Uid { get; } /// - /// Returns the main display name of this user from the Firebase project's user database. + /// Returns the main display name of this user from the Firebase project's user database. /// - string DisplayName { get; } + string? DisplayName { get; } /// /// Returns the main email address of the user, as stored in the Firebase project's user database. /// - string Email { get; } + string? Email { get; } /// - /// Returns the URL of this user's main profile picture, as stored in the Firebase project's user database. + /// Returns the URL of this user's main profile picture, as stored in the Firebase project's user database. /// - string PhotoUrl { get; } + string? PhotoUrl { get; } /// /// Always returns FirebaseAuthProvider.PROVIDER_ID. @@ -98,10 +98,10 @@ public interface IFirebaseUser /// /// Profile data for each identity provider, if any. This data is cached on sign-in and updated when linking or unlinking. /// - IEnumerable ProviderInfos { get; } + IEnumerable? ProviderInfos { get; } /// /// Metadata associated with the Firebase user in question. /// - UserMetadata Metadata { get; } + UserMetadata? Metadata { get; } } \ No newline at end of file diff --git a/src/Auth/Shared/ProviderInfo.cs b/src/Auth/Shared/ProviderInfo.cs index acbc90cc..a340cdb0 100644 --- a/src/Auth/Shared/ProviderInfo.cs +++ b/src/Auth/Shared/ProviderInfo.cs @@ -1,8 +1,27 @@ namespace Plugin.Firebase.Auth; +/// +/// Represents profile data from a third-party identity provider associated with a Firebase user. +/// public class ProviderInfo { - public ProviderInfo(string uid, string providerId, string displayName, string email, string phoneNumber, string photoUrl) + /// + /// Creates a new ProviderInfo instance. + /// + /// The user's unique ID within the provider. + /// The ID of the identity provider. + /// The user's display name. + /// The user's email address. + /// The user's phone number. + /// The URL of the user's profile photo. + public ProviderInfo( + string uid, + string providerId, + string? displayName, + string? email, + string? phoneNumber, + string? photoUrl + ) { Uid = uid; ProviderId = providerId; @@ -12,10 +31,33 @@ public ProviderInfo(string uid, string providerId, string displayName, string em PhotoUrl = photoUrl; } + /// + /// Gets the user's unique ID within the provider. + /// public string Uid { get; } + + /// + /// Gets the ID of the identity provider (e.g., "google.com", "facebook.com"). + /// public string ProviderId { get; } - public string DisplayName { get; } - public string Email { get; } - public string PhoneNumber { get; } - public string PhotoUrl { get; } + + /// + /// Gets the user's display name from the provider. + /// + public string? DisplayName { get; } + + /// + /// Gets the user's email address from the provider. + /// + public string? Email { get; } + + /// + /// Gets the user's phone number from the provider. + /// + public string? PhoneNumber { get; } + + /// + /// Gets the URL of the user's profile photo from the provider. + /// + public string? PhotoUrl { get; } } \ No newline at end of file diff --git a/src/Auth/Shared/UserMetadata.cs b/src/Auth/Shared/UserMetadata.cs index c08b7284..89e1987a 100644 --- a/src/Auth/Shared/UserMetadata.cs +++ b/src/Auth/Shared/UserMetadata.cs @@ -1,13 +1,28 @@ namespace Plugin.Firebase.Auth; +/// +/// Represents metadata about a Firebase user. +/// public sealed class UserMetadata { + /// + /// Creates a new UserMetadata instance. + /// + /// The date and time the user account was created. + /// The date and time of the user's last sign-in. public UserMetadata(DateTimeOffset creationDate, DateTimeOffset lastSignInDate) { CreationDate = creationDate; LastSignInDate = lastSignInDate; } + /// + /// Gets the date and time the user account was created. + /// public DateTimeOffset CreationDate { get; } + + /// + /// Gets the date and time of the user's last sign-in. + /// public DateTimeOffset LastSignInDate { get; } } \ No newline at end of file diff --git a/src/Bundled/Bundled.csproj b/src/Bundled/Bundled.csproj index 6bb7114c..47fd8ae7 100644 --- a/src/Bundled/Bundled.csproj +++ b/src/Bundled/Bundled.csproj @@ -22,7 +22,7 @@ Plugin.Firebase - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase @@ -54,6 +54,10 @@ pdbonly true + + + false + diff --git a/src/Bundled/Platforms/Android/CrossFirebase.cs b/src/Bundled/Platforms/Android/CrossFirebase.cs index 987d8c11..5b70c209 100644 --- a/src/Bundled/Platforms/Android/CrossFirebase.cs +++ b/src/Bundled/Platforms/Android/CrossFirebase.cs @@ -5,14 +5,26 @@ namespace Plugin.Firebase.Bundled.Platforms.Android; +/// +/// Android-specific bundled Firebase initialization entry point. +/// public static class CrossFirebase { + /// + /// Initializes Firebase with all configured services on Android. + /// + /// The current activity. + /// A delegate that returns the current Android activity. + /// The bundled settings specifying which services to enable. + /// Optional Firebase configuration options. + /// Optional name for the Firebase app instance. public static void Initialize( Activity activity, Func activityLocator, CrossFirebaseSettings settings, FirebaseOptions firebaseOptions = null, - string name = null) + string name = null + ) { Core.Platforms.Android.CrossFirebase.RegisterActivityLocator(activityLocator); @@ -32,4 +44,4 @@ public static void Initialize( Console.WriteLine($"Plugin.Firebase initialized with the following settings:\n{settings}"); } -} +} \ No newline at end of file diff --git a/src/Bundled/Platforms/iOS/CrossFirebase.cs b/src/Bundled/Platforms/iOS/CrossFirebase.cs index 6f18a9b8..ff0f3ba0 100644 --- a/src/Bundled/Platforms/iOS/CrossFirebase.cs +++ b/src/Bundled/Platforms/iOS/CrossFirebase.cs @@ -1,16 +1,26 @@ using Firebase.Core; +using Plugin.Firebase.Bundled.Shared; using Plugin.Firebase.CloudMessaging; using Plugin.Firebase.Crashlytics; -using Plugin.Firebase.Bundled.Shared; namespace Plugin.Firebase.Bundled.Platforms.iOS; +/// +/// iOS-specific bundled Firebase initialization entry point. +/// public static class CrossFirebase { + /// + /// Initializes Firebase with all configured services on iOS. + /// + /// The bundled settings specifying which services to enable. + /// Optional Firebase configuration options. + /// Optional name for the Firebase app instance. public static void Initialize( CrossFirebaseSettings settings, Options firebaseOptions = null, - string name = null) + string name = null + ) { if(firebaseOptions == null) { App.Configure(); @@ -24,8 +34,10 @@ public static void Initialize( FirebaseCloudMessagingImplementation.Initialize(); } - CrossFirebaseCrashlytics.Current.SetCrashlyticsCollectionEnabled(settings.IsCrashlyticsEnabled); + CrossFirebaseCrashlytics.Current.SetCrashlyticsCollectionEnabled( + settings.IsCrashlyticsEnabled + ); Console.WriteLine($"Plugin.Firebase initialized with the following settings:\n{settings}"); } -} +} \ No newline at end of file diff --git a/src/Bundled/Shared/CrossFirebaseSettings.cs b/src/Bundled/Shared/CrossFirebaseSettings.cs index 025781c3..ea788656 100644 --- a/src/Bundled/Shared/CrossFirebaseSettings.cs +++ b/src/Bundled/Shared/CrossFirebaseSettings.cs @@ -1,7 +1,23 @@ namespace Plugin.Firebase.Bundled.Shared; +/// +/// Configuration settings for Plugin.Firebase bundled initialization. +/// public sealed class CrossFirebaseSettings { + /// + /// Creates a new CrossFirebaseSettings instance. + /// + /// Whether Firebase Analytics is enabled. + /// Whether Firebase Authentication is enabled. + /// Whether Firebase Cloud Messaging is enabled. + /// Whether Firebase Crashlytics is enabled. + /// Whether Firebase Dynamic Links is enabled. + /// Whether Firebase Firestore is enabled. + /// Whether Firebase Functions is enabled. + /// Whether Firebase Remote Config is enabled. + /// Whether Firebase Storage is enabled. + /// The Google request ID token for Google Sign-In. public CrossFirebaseSettings( bool isAnalyticsEnabled = false, bool isAuthEnabled = false, @@ -12,7 +28,8 @@ public CrossFirebaseSettings( bool isFunctionsEnabled = false, bool isRemoteConfigEnabled = false, bool isStorageEnabled = false, - string googleRequestIdToken = null) + string googleRequestIdToken = null + ) { IsAnalyticsEnabled = isAnalyticsEnabled; IsAuthEnabled = isAuthEnabled; @@ -26,29 +43,68 @@ public CrossFirebaseSettings( GoogleRequestIdToken = googleRequestIdToken; } + /// public override string ToString() { - return $"[{nameof(CrossFirebaseSettings)}: " + - $"{nameof(IsAnalyticsEnabled)}={IsAnalyticsEnabled}," + - $"{nameof(IsAuthEnabled)}={IsAuthEnabled}," + - $"{nameof(IsCloudMessagingEnabled)}={IsCloudMessagingEnabled}," + - $"{nameof(IsCrashlyticsEnabled)}={IsCrashlyticsEnabled}," + - $"{nameof(IsDynamicLinksEnabled)}={IsDynamicLinksEnabled}," + - $"{nameof(IsFirestoreEnabled)}={IsFirestoreEnabled}," + - $"{nameof(IsFunctionsEnabled)}={IsFunctionsEnabled}," + - $"{nameof(IsRemoteConfigEnabled)}={IsRemoteConfigEnabled}," + - $"{nameof(IsStorageEnabled)}={IsStorageEnabled}]"; + return $"[{nameof(CrossFirebaseSettings)}: " + + $"{nameof(IsAnalyticsEnabled)}={IsAnalyticsEnabled}," + + $"{nameof(IsAuthEnabled)}={IsAuthEnabled}," + + $"{nameof(IsCloudMessagingEnabled)}={IsCloudMessagingEnabled}," + + $"{nameof(IsCrashlyticsEnabled)}={IsCrashlyticsEnabled}," + + $"{nameof(IsDynamicLinksEnabled)}={IsDynamicLinksEnabled}," + + $"{nameof(IsFirestoreEnabled)}={IsFirestoreEnabled}," + + $"{nameof(IsFunctionsEnabled)}={IsFunctionsEnabled}," + + $"{nameof(IsRemoteConfigEnabled)}={IsRemoteConfigEnabled}," + + $"{nameof(IsStorageEnabled)}={IsStorageEnabled}]"; } + /// + /// Gets whether Firebase Analytics is enabled. + /// public bool IsAnalyticsEnabled { get; } + + /// + /// Gets whether Firebase Authentication is enabled. + /// public bool IsAuthEnabled { get; } + + /// + /// Gets whether Firebase Cloud Messaging is enabled. + /// public bool IsCloudMessagingEnabled { get; } + + /// + /// Gets whether Firebase Crashlytics is enabled. + /// public bool IsCrashlyticsEnabled { get; } + + /// + /// Gets whether Firebase Dynamic Links is enabled. + /// public bool IsDynamicLinksEnabled { get; } + + /// + /// Gets whether Firebase Firestore is enabled. + /// public bool IsFirestoreEnabled { get; } + + /// + /// Gets whether Firebase Functions is enabled. + /// public bool IsFunctionsEnabled { get; } + + /// + /// Gets whether Firebase Remote Config is enabled. + /// public bool IsRemoteConfigEnabled { get; } + + /// + /// Gets whether Firebase Storage is enabled. + /// public bool IsStorageEnabled { get; } + /// + /// Gets the Google request ID token for Google Sign-In. + /// public string GoogleRequestIdToken { get; } -} +} \ No newline at end of file diff --git a/src/CloudMessaging/CloudMessaging.csproj b/src/CloudMessaging/CloudMessaging.csproj index 6f48c862..406f89b3 100644 --- a/src/CloudMessaging/CloudMessaging.csproj +++ b/src/CloudMessaging/CloudMessaging.csproj @@ -23,7 +23,7 @@ Plugin.Firebase.CloudMessaging - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/CloudMessaging/Platforms/Android/Extensions/DictionaryExtensions.cs b/src/CloudMessaging/Platforms/Android/Extensions/DictionaryExtensions.cs index 799083dc..2c2341fc 100644 --- a/src/CloudMessaging/Platforms/Android/Extensions/DictionaryExtensions.cs +++ b/src/CloudMessaging/Platforms/Android/Extensions/DictionaryExtensions.cs @@ -172,24 +172,14 @@ private static object GetObject(this Bundle bundle, string key) return bdl; } - if(bundle.GetParcelable(key) is { } parcelable) { - return parcelable; - } - - if(bundle.GetParcelableArray(key) is { } parcelableArray) { - return parcelableArray; - } - - if(bundle.GetParcelableArrayList(key) is { } parcelableArrayList) { - return parcelableArrayList; - } - - if(bundle.GetSparseParcelableArray(key) is { } sparseParcelableArray) { - return sparseParcelableArray; - } - - if(bundle.GetSerializable(key) is { } serializable) { - return serializable; + // Prefer Bundle.Get(key) for “unknown / mixed” extras. + // This avoids relying on APIs that are obsoleted on newer Android versions (API 33+). + if(bundle.Get(key) is { } raw) { + if(raw is Java.Lang.Boolean) { + return bundle.GetBoolean(key); + } + + return raw; } return bundle.GetBoolean(key); diff --git a/src/CloudMessaging/Platforms/iOS/Extensions/FCMNotificationExtensions.cs b/src/CloudMessaging/Platforms/iOS/Extensions/FCMNotificationExtensions.cs index 52f3e41f..978053ae 100644 --- a/src/CloudMessaging/Platforms/iOS/Extensions/FCMNotificationExtensions.cs +++ b/src/CloudMessaging/Platforms/iOS/Extensions/FCMNotificationExtensions.cs @@ -2,31 +2,59 @@ namespace Plugin.Firebase.CloudMessaging.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting iOS notification types to . +/// public static class FCMNotificationExtension { + /// + /// Converts a to an . + /// + /// The iOS notification to convert. + /// An containing the notification data. public static FCMNotification ToFCMNotification(this UNNotification notification) { return notification.Request.ToFCMNotification(); } + /// + /// Converts a to an . + /// + /// The iOS notification request to convert. + /// An containing the notification data. public static FCMNotification ToFCMNotification(this UNNotificationRequest request) { return request.Content.UserInfo.ToFCMNotification(); } + /// + /// Converts an containing notification user info to an . + /// + /// The user info dictionary from the notification. + /// An containing the notification data, or an empty notification if parsing fails. public static FCMNotification ToFCMNotification(this NSDictionary userInfo) { if(userInfo["aps"] is NSDictionary apsDict) { var alert = apsDict["alert"]; if(alert is NSDictionary dict) { - return new FCMNotification(GetBody(dict), GetTitle(dict), GetImageUrl(userInfo), userInfo.ToDictionary()); + return new FCMNotification( + GetBody(dict), + GetTitle(dict), + GetImageUrl(userInfo), + userInfo.ToDictionary() + ); } else if(alert != null) { return new FCMNotification(alert.ToString(), "", "", userInfo.ToDictionary()); } } else { var notification = userInfo["notification"]; if(notification is NSDictionary dict) { - return new FCMNotification(GetBody(dict), GetTitle(dict), GetImageUrl(dict), userInfo.ToDictionary()); + return new FCMNotification( + GetBody(dict), + GetTitle(dict), + GetImageUrl(dict), + userInfo.ToDictionary() + ); } } return FCMNotification.Empty(); diff --git a/src/CloudMessaging/Platforms/iOS/FirebaseCloudMessagingImplementation.cs b/src/CloudMessaging/Platforms/iOS/FirebaseCloudMessagingImplementation.cs index 3b8a15ae..af084219 100644 --- a/src/CloudMessaging/Platforms/iOS/FirebaseCloudMessagingImplementation.cs +++ b/src/CloudMessaging/Platforms/iOS/FirebaseCloudMessagingImplementation.cs @@ -6,11 +6,21 @@ namespace Plugin.Firebase.CloudMessaging; +/// +/// iOS implementation of that wraps the native Firebase Cloud Messaging SDK. +/// [Preserve(AllMembers = true)] -public sealed class FirebaseCloudMessagingImplementation : NSObject, IFirebaseCloudMessaging, IUNUserNotificationCenterDelegate, IMessagingDelegate +public sealed class FirebaseCloudMessagingImplementation + : NSObject, + IFirebaseCloudMessaging, + IUNUserNotificationCenterDelegate, + IMessagingDelegate { private FCMNotification _missedTappedNotification; + /// + /// Initializes the Firebase Cloud Messaging service and registers for remote notifications. + /// public static void Initialize() { var instance = (FirebaseCloudMessagingImplementation) CrossFirebaseCloudMessaging.Current; @@ -25,12 +35,22 @@ private void RegisterForRemoteNotifications() Messaging.SharedInstance.Delegate = this; UIApplication.SharedApplication.RegisterForRemoteNotifications(); } else { - var allNotificationTypes = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound; - var settings = UIUserNotificationSettings.GetSettingsForTypes(allNotificationTypes, null); + var allNotificationTypes = + UIUserNotificationType.Alert + | UIUserNotificationType.Badge + | UIUserNotificationType.Sound; + var settings = UIUserNotificationSettings.GetSettingsForTypes( + allNotificationTypes, + null + ); UIApplication.SharedApplication.RegisterUserNotificationSettings(settings); } } + /// + /// Triggers a token refresh check and raises the event if a token is available. + /// + /// A completed task. public Task OnTokenRefreshAsync() { OnTokenRefresh(Messaging.SharedInstance.FcmToken); @@ -42,6 +62,7 @@ private void OnTokenRefresh(string fcmToken) TokenChanged?.Invoke(this, new FCMTokenChangedEventArgs(fcmToken)); } + /// public Task CheckIfValidAsync() { if(UIDevice.CurrentDevice.CheckSystemVersion(10, 0)) { @@ -53,84 +74,142 @@ public Task CheckIfValidAsync() private void RequestAuthorization() { UNUserNotificationCenter.Current.RequestAuthorization( - UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound, + UNAuthorizationOptions.Alert + | UNAuthorizationOptions.Badge + | UNAuthorizationOptions.Sound, (granted, _) => { if(!granted) { - Error?.Invoke(this, new FCMErrorEventArgs("User permission for remote notifications is not granted")); + Error?.Invoke( + this, + new FCMErrorEventArgs( + "User permission for remote notifications is not granted" + ) + ); } - }); + } + ); } + /// + /// Handles notification presentation when the app is in the foreground. + /// + /// The notification center. + /// The notification being presented. + /// The completion handler to call with presentation options. [Export("userNotificationCenter:willPresentNotification:withCompletionHandler:")] - public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action completionHandler) + public void WillPresentNotification( + UNUserNotificationCenter center, + UNNotification notification, + Action completionHandler + ) { var fcmNotification = notification.ToFCMNotification(); OnNotificationReceived(fcmNotification); if(!fcmNotification.IsSilentInForeground) { if(OperatingSystem.IsIOSVersionAtLeast(14)) { - completionHandler(UNNotificationPresentationOptions.Banner - | UNNotificationPresentationOptions.List - | UNNotificationPresentationOptions.Sound); - + completionHandler( + UNNotificationPresentationOptions.Banner + | UNNotificationPresentationOptions.List + | UNNotificationPresentationOptions.Sound + ); } else { - completionHandler(UNNotificationPresentationOptions.Alert - | UNNotificationPresentationOptions.Sound); + completionHandler( + UNNotificationPresentationOptions.Alert + | UNNotificationPresentationOptions.Sound + ); } } } + /// + /// Raises the event with the specified notification. + /// + /// The received FCM notification. public void OnNotificationReceived(FCMNotification message) { NotificationReceived?.Invoke(this, new FCMNotificationReceivedEventArgs(message)); } + /// + /// Handles the user's response to a notification (e.g., tapping on it). + /// + /// The notification center. + /// The user's response to the notification. + /// The completion handler to call when processing is complete. [Export("userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:")] - public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler) + public void DidReceiveNotificationResponse( + UNUserNotificationCenter center, + UNNotificationResponse response, + Action completionHandler + ) { if(_notificationTapped == null) { _missedTappedNotification = response.Notification.ToFCMNotification(); } else { - _notificationTapped.Invoke(this, new FCMNotificationTappedEventArgs(response.Notification.ToFCMNotification())); + _notificationTapped.Invoke( + this, + new FCMNotificationTappedEventArgs(response.Notification.ToFCMNotification()) + ); } completionHandler(); } + /// + /// Called when a new FCM registration token is received. + /// + /// The messaging instance. + /// The new FCM token. [Export("messaging:didReceiveRegistrationToken:")] public void DidReceiveRegistrationToken(Messaging messaging, string fcmToken) { OnTokenRefresh(fcmToken); } + /// public Task GetTokenAsync() { var token = Messaging.SharedInstance.FcmToken; - return string.IsNullOrEmpty(token) ? throw new FirebaseException("Couldn't retrieve FCM token") : Task.FromResult(token); + return string.IsNullOrEmpty(token) + ? throw new FirebaseException("Couldn't retrieve FCM token") + : Task.FromResult(token); } + /// public Task SubscribeToTopicAsync(string topic) { return Messaging.SharedInstance.SubscribeAsync(topic); } + /// public Task UnsubscribeFromTopicAsync(string topic) { return Messaging.SharedInstance.UnsubscribeAsync(topic); } + /// public event EventHandler TokenChanged; + + /// public event EventHandler NotificationReceived; + + /// public event EventHandler Error; private event EventHandler _notificationTapped; + + /// public event EventHandler NotificationTapped { add { _notificationTapped += value; if(_missedTappedNotification != null) { - _notificationTapped?.Invoke(this, new FCMNotificationTappedEventArgs(_missedTappedNotification)); + _notificationTapped?.Invoke( + this, + new FCMNotificationTappedEventArgs(_missedTappedNotification) + ); _missedTappedNotification = null; } } remove => _notificationTapped -= value; } -} +} \ No newline at end of file diff --git a/src/CloudMessaging/Shared/CrossFirebaseCloudMessaging.cs b/src/CloudMessaging/Shared/CrossFirebaseCloudMessaging.cs index 43b8c01b..70ebd63e 100644 --- a/src/CloudMessaging/Shared/CrossFirebaseCloudMessaging.cs +++ b/src/CloudMessaging/Shared/CrossFirebaseCloudMessaging.cs @@ -1,8 +1,12 @@ namespace Plugin.Firebase.CloudMessaging { + /// + /// Cross-platform entry point for Firebase Cloud Messaging. + /// public sealed class CrossFirebaseCloudMessaging { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = + new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); private static IFirebaseCloudMessaging CreateInstance() { @@ -34,16 +38,21 @@ public static IFirebaseCloudMessaging Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } diff --git a/src/CloudMessaging/Shared/EventArgs/FCMErrorEventArgs.cs b/src/CloudMessaging/Shared/EventArgs/FCMErrorEventArgs.cs index ae84ae42..a164d30e 100644 --- a/src/CloudMessaging/Shared/EventArgs/FCMErrorEventArgs.cs +++ b/src/CloudMessaging/Shared/EventArgs/FCMErrorEventArgs.cs @@ -1,12 +1,22 @@ namespace Plugin.Firebase.CloudMessaging.EventArgs { + /// + /// Event arguments for Firebase Cloud Messaging error events. + /// public sealed class FCMErrorEventArgs : System.EventArgs { + /// + /// Creates a new FCMErrorEventArgs instance. + /// + /// The error message. public FCMErrorEventArgs(string message) { Message = message; } + /// + /// Gets the error message. + /// public string Message { get; } } } \ No newline at end of file diff --git a/src/CloudMessaging/Shared/EventArgs/FCMNotificationReceivedEventArgs.cs b/src/CloudMessaging/Shared/EventArgs/FCMNotificationReceivedEventArgs.cs index 1f8ad0e5..b71a1a4c 100644 --- a/src/CloudMessaging/Shared/EventArgs/FCMNotificationReceivedEventArgs.cs +++ b/src/CloudMessaging/Shared/EventArgs/FCMNotificationReceivedEventArgs.cs @@ -1,12 +1,22 @@ namespace Plugin.Firebase.CloudMessaging.EventArgs { + /// + /// Event arguments for when a Firebase Cloud Messaging notification is received. + /// public sealed class FCMNotificationReceivedEventArgs : System.EventArgs { + /// + /// Creates a new FCMNotificationReceivedEventArgs instance. + /// + /// The received notification. public FCMNotificationReceivedEventArgs(FCMNotification notification) { Notification = notification; } + /// + /// Gets the received notification. + /// public FCMNotification Notification { get; } } } \ No newline at end of file diff --git a/src/CloudMessaging/Shared/EventArgs/FCMNotificationTappedEventArgs.cs b/src/CloudMessaging/Shared/EventArgs/FCMNotificationTappedEventArgs.cs index ce5310e7..2e1b363b 100644 --- a/src/CloudMessaging/Shared/EventArgs/FCMNotificationTappedEventArgs.cs +++ b/src/CloudMessaging/Shared/EventArgs/FCMNotificationTappedEventArgs.cs @@ -1,12 +1,22 @@ namespace Plugin.Firebase.CloudMessaging.EventArgs { + /// + /// Event arguments for when a Firebase Cloud Messaging notification is tapped by the user. + /// public sealed class FCMNotificationTappedEventArgs { + /// + /// Creates a new FCMNotificationTappedEventArgs instance. + /// + /// The tapped notification. public FCMNotificationTappedEventArgs(FCMNotification notification) { Notification = notification; } + /// + /// Gets the tapped notification. + /// public FCMNotification Notification { get; } } } \ No newline at end of file diff --git a/src/CloudMessaging/Shared/EventArgs/FCMTokenChangedEventArgs.cs b/src/CloudMessaging/Shared/EventArgs/FCMTokenChangedEventArgs.cs index 6e8a2080..84ed4e42 100644 --- a/src/CloudMessaging/Shared/EventArgs/FCMTokenChangedEventArgs.cs +++ b/src/CloudMessaging/Shared/EventArgs/FCMTokenChangedEventArgs.cs @@ -1,12 +1,22 @@ namespace Plugin.Firebase.CloudMessaging.EventArgs { + /// + /// Event arguments for when the Firebase Cloud Messaging registration token changes. + /// public sealed class FCMTokenChangedEventArgs : System.EventArgs { + /// + /// Creates a new FCMTokenChangedEventArgs instance. + /// + /// The new FCM registration token. public FCMTokenChangedEventArgs(string token) { Token = token; } + /// + /// Gets the new FCM registration token. + /// public string Token { get; } } } \ No newline at end of file diff --git a/src/CloudMessaging/Shared/FCMException.cs b/src/CloudMessaging/Shared/FCMException.cs index 0317c53e..6472989c 100644 --- a/src/CloudMessaging/Shared/FCMException.cs +++ b/src/CloudMessaging/Shared/FCMException.cs @@ -2,11 +2,16 @@ namespace Plugin.Firebase.CloudMessaging { + /// + /// Exception thrown when a Firebase Cloud Messaging operation fails. + /// public sealed class FCMException : FirebaseException { + /// + /// Creates a new FCMException with the specified message. + /// + /// The error message. public FCMException(string message) - : base(message) - { - } + : base(message) { } } } \ No newline at end of file diff --git a/src/CloudMessaging/Shared/FCMNotification.cs b/src/CloudMessaging/Shared/FCMNotification.cs index 5e802384..47310a82 100644 --- a/src/CloudMessaging/Shared/FCMNotification.cs +++ b/src/CloudMessaging/Shared/FCMNotification.cs @@ -1,7 +1,14 @@ namespace Plugin.Firebase.CloudMessaging { + /// + /// Represents a Firebase Cloud Messaging notification. + /// public sealed class FCMNotification { + /// + /// Creates an empty notification instance. + /// + /// An empty FCMNotification. public static FCMNotification Empty() { return new FCMNotification(); @@ -10,11 +17,19 @@ public static FCMNotification Empty() private readonly string _body; private readonly string _title; + /// + /// Creates a new FCMNotification instance. + /// + /// The notification body text. + /// The notification title. + /// The URL of an image to display in the notification. + /// Additional data payload as key-value pairs. public FCMNotification( string body = null, string title = null, string imageUrl = null, - IDictionary data = null) + IDictionary data = null + ) { _body = body; _title = title; @@ -22,16 +37,41 @@ public FCMNotification( Data = data; } + /// public override string ToString() { return $"[FCMNotification: Body={Body}, Title={Title}, Data={(Data == null ? "" : string.Join(", ", Data.Select(kvp => $"{kvp.Key}:{kvp.Value}")))}]"; } + /// + /// Gets the notification body text. + /// + public string Body => + _body ?? (Data != null && Data.ContainsKey("body") ? Data["body"] : ""); - public string Body => _body ?? (Data != null && Data.ContainsKey("body") ? Data["body"] : ""); - public string Title => _title ?? (Data != null && Data.ContainsKey("title") ? Data["title"] : ""); - public bool IsSilentInForeground => Data != null && Data.ContainsKey("is_silent_in_foreground") && bool.TryParse(Data["is_silent_in_foreground"], out var value) && value; + /// + /// Gets the notification title. + /// + public string Title => + _title ?? (Data != null && Data.ContainsKey("title") ? Data["title"] : ""); + + /// + /// Gets whether the notification should be silent in the foreground. + /// + public bool IsSilentInForeground => + Data != null + && Data.ContainsKey("is_silent_in_foreground") + && bool.TryParse(Data["is_silent_in_foreground"], out var value) + && value; + + /// + /// Gets the URL of an image to display in the notification. + /// public string ImageUrl { get; } + + /// + /// Gets the additional data payload as key-value pairs. + /// public IDictionary Data { get; } } -} +} \ No newline at end of file diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 78d11f9e..4ee186e9 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,9 +21,9 @@ $(DefineConstants); - - Plugin.Firebase.Core - 4.0.1 + + Plugin.Firebase.Core + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/Core/Platforms/Android/CrossFirebase.cs b/src/Core/Platforms/Android/CrossFirebase.cs index b6f9d189..3820cc4c 100644 --- a/src/Core/Platforms/Android/CrossFirebase.cs +++ b/src/Core/Platforms/Android/CrossFirebase.cs @@ -2,23 +2,29 @@ namespace Plugin.Firebase.Core.Platforms.Android; +/// +/// Android-specific initialization entry point for Firebase. +/// public static class CrossFirebase { - public static Func ActivityLocator - { - get; - private set; - } + /// + /// Gets the activity locator delegate used to obtain the current Android activity. + /// + public static Func? ActivityLocator { get; private set; } + /// + /// Initializes Firebase on Android. + /// /// The current activity. This will be passed into the platform FirebaseApp.Initialize method. - /// A delegate that should always return the 'current' activity. This will be invoked whenever the plugin needs to pass an activity into native Firebases SDK methods. - /// - /// + /// A delegate that should always return the 'current' activity. This will be invoked whenever the plugin needs to pass an activity into native Firebase SDK methods. + /// Optional Firebase configuration options. + /// Optional name for the Firebase app instance. public static void Initialize( Activity activity, Func activityLocator, - FirebaseOptions firebaseOptions = null, - string name = null) + FirebaseOptions? firebaseOptions = null, + string? name = null + ) { RegisterActivityLocator(activityLocator); @@ -31,6 +37,10 @@ public static void Initialize( } } + /// + /// Registers an activity locator delegate. + /// + /// A delegate that returns the current Android activity. public static void RegisterActivityLocator(Func activityLocator) { ActivityLocator = activityLocator; diff --git a/src/Core/Platforms/Android/OnCompleteListener.cs b/src/Core/Platforms/Android/OnCompleteListener.cs index 63b37011..4b069364 100644 --- a/src/Core/Platforms/Android/OnCompleteListener.cs +++ b/src/Core/Platforms/Android/OnCompleteListener.cs @@ -3,15 +3,26 @@ namespace Plugin.Firebase.Core.Platforms.Android; +/// +/// Android callback adapter that wraps a task completion delegate for use with Google Play Services Task API. +/// public class OnCompleteListener : Java.Lang.Object, IOnCompleteListener { private readonly Action _action; + /// + /// Initializes a new instance of the class with the specified completion action. + /// + /// The delegate to invoke when the task completes (success or failure). public OnCompleteListener(Action action) { _action = action; } + /// + /// Called when the task completes. + /// + /// The completed task. public void OnComplete(Task task) { _action?.Invoke(task); diff --git a/src/Core/Platforms/Android/OnFailureListener.cs b/src/Core/Platforms/Android/OnFailureListener.cs index cea0a7e6..c49a0494 100644 --- a/src/Core/Platforms/Android/OnFailureListener.cs +++ b/src/Core/Platforms/Android/OnFailureListener.cs @@ -4,15 +4,26 @@ namespace Plugin.Firebase.Core.Platforms.Android; +/// +/// Android callback adapter that wraps a failure handler delegate for use with Google Play Services Task API. +/// public sealed class OnFailureListener : Object, IOnFailureListener { private readonly Action _action; + /// + /// Initializes a new instance of the class with the specified failure handler. + /// + /// The delegate to invoke when the task fails. public OnFailureListener(Action action) { _action = action; } + /// + /// Called when the task fails. + /// + /// The exception that caused the task to fail. public void OnFailure(Exception e) { _action(e); diff --git a/src/Core/Platforms/Android/OnSuccessListener.cs b/src/Core/Platforms/Android/OnSuccessListener.cs index 7e076a5e..c9114fcc 100644 --- a/src/Core/Platforms/Android/OnSuccessListener.cs +++ b/src/Core/Platforms/Android/OnSuccessListener.cs @@ -3,17 +3,28 @@ namespace Plugin.Firebase.Core.Platforms.Android; +/// +/// Android callback adapter that wraps a success action delegate for use with Google Play Services Task API. +/// public sealed class OnSuccessListener : Object, IOnSuccessListener { private readonly Action _action; + /// + /// Initializes a new instance of the class with the specified success action. + /// + /// The delegate to invoke when the task completes successfully. public OnSuccessListener(Action action) { _action = action; } - public void OnSuccess(Object result) + /// + /// Called when the task completes successfully. + /// + /// The result of the completed task. + public void OnSuccess(Object? result) { - _action(result); + _action(result!); } } \ No newline at end of file diff --git a/src/Core/Platforms/iOS/CrossFirebase.cs b/src/Core/Platforms/iOS/CrossFirebase.cs index fc05754c..204afadd 100644 --- a/src/Core/Platforms/iOS/CrossFirebase.cs +++ b/src/Core/Platforms/iOS/CrossFirebase.cs @@ -2,9 +2,17 @@ namespace Plugin.Firebase.Core.Platforms.iOS; +/// +/// iOS-specific initialization entry point for Firebase. +/// public static class CrossFirebase { - public static void Initialize(string name = null, Options firebaseOptions = null) + /// + /// Initializes Firebase on iOS. + /// + /// Optional name for the Firebase app instance. + /// Optional Firebase configuration options. + public static void Initialize(string? name = null, Options? firebaseOptions = null) { if(firebaseOptions == null) { App.Configure(); diff --git a/src/Core/Platforms/iOS/Extensions/DateExtensions.cs b/src/Core/Platforms/iOS/Extensions/DateExtensions.cs index 5b4a770f..16ac405f 100644 --- a/src/Core/Platforms/iOS/Extensions/DateExtensions.cs +++ b/src/Core/Platforms/iOS/Extensions/DateExtensions.cs @@ -1,7 +1,15 @@ namespace Plugin.Firebase.Core.Platforms.iOS.Extensions; +/// +/// Extension methods for converting between .NET date types and iOS NSDate. +/// public static class DateExtensions { + /// + /// Converts a to an . + /// + /// The DateTime to convert. + /// An NSDate representing the same point in time. public static NSDate ToNSDate(this DateTime @this) { if(@this.Kind == DateTimeKind.Unspecified) { @@ -10,13 +18,25 @@ public static NSDate ToNSDate(this DateTime @this) return (NSDate) @this; } + /// + /// Converts a to an . + /// + /// The DateTimeOffset to convert. + /// An NSDate representing the same point in time. public static NSDate ToNSDate(this DateTimeOffset @this) { return @this.DateTime.ToNSDate(); } + /// + /// Converts an to a . + /// + /// The NSDate to convert. + /// A DateTimeOffset representing the same point in time, or default if null. public static DateTimeOffset ToDateTimeOffset(this NSDate @this) { - return @this == null ? default(DateTimeOffset) : DateTime.SpecifyKind((DateTime) @this, DateTimeKind.Utc); + return @this == null + ? default(DateTimeOffset) + : DateTime.SpecifyKind((DateTime) @this, DateTimeKind.Utc); } } \ No newline at end of file diff --git a/src/Core/Shared/DisposableBase.cs b/src/Core/Shared/DisposableBase.cs index f0e245f7..57dfcdb1 100644 --- a/src/Core/Shared/DisposableBase.cs +++ b/src/Core/Shared/DisposableBase.cs @@ -1,20 +1,35 @@ namespace Plugin.Firebase.Core; +/// +/// Base class implementing the standard pattern. +/// public class DisposableBase : IDisposable { private bool _disposed; + /// + /// Releases resources used by this instance. + /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// + /// Finalizer. + /// ~DisposableBase() { Dispose(false); } + /// + /// Releases resources used by this instance. + /// + /// + /// true when called from ; false when called from the finalizer. + /// public virtual void Dispose(bool disposing) { if(!_disposed) { diff --git a/src/Core/Shared/DisposableWithAction.cs b/src/Core/Shared/DisposableWithAction.cs index 149b12f8..4159946c 100644 --- a/src/Core/Shared/DisposableWithAction.cs +++ b/src/Core/Shared/DisposableWithAction.cs @@ -1,14 +1,22 @@ namespace Plugin.Firebase.Core; +/// +/// implementation that executes an when disposed. +/// public sealed class DisposableWithAction : DisposableBase { private readonly Action _action; + /// + /// Creates a new instance. + /// + /// Action to execute when disposing. public DisposableWithAction(Action action) { _action = action; } + /// public override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/src/Core/Shared/Exceptions/FirebaseAuthException.cs b/src/Core/Shared/Exceptions/FirebaseAuthException.cs index 3d7f7dc7..ee9fb5f4 100644 --- a/src/Core/Shared/Exceptions/FirebaseAuthException.cs +++ b/src/Core/Shared/Exceptions/FirebaseAuthException.cs @@ -1,206 +1,292 @@ namespace Plugin.Firebase.Core.Exceptions; +/// +/// Cross-platform Firebase Auth error reasons. +/// public enum FIRAuthError { /// /// Unknown error reason. /// Undefined = 0, + /// /// Indicates the email address is malformed. /// InvalidEmail = 1, + /// /// Indicates the user attempted sign in with a wrong password. /// WrongPassword = 2, + /// /// Indicates an attempt to set a password that is considered too weak. /// WeakPassword = 3, + /// /// Indicates the email used to attempt sign up already exists. /// EmailAlreadyInUse = 4, + /// /// Indicates the user account was not found. /// UserNotFound = 5, + /// /// Indicates the current user’s token has expired, for example, the user may have changed account password on another device. /// You must prompt the user to sign in again on this device. /// UserTokenExpired = 6, + /// /// Indicates the supplied credential is invalid. This could happen if it has expired or it is malformed. /// InvalidCredential = 7, + /// /// Indicates the user's account is disabled. /// UserDisabled = 8, + /// /// Indicates the user already signed in once with a trusted provider and hence cannot sign in with another provider. /// List of trusted and untrusted providers: https://firebase.google.com/docs/auth/users#verified_email_addresses /// AccountExistsWithDifferentCredential = 9, + /// /// Indicates a validation error with the custom token. /// InvalidCustomToken = 10, + /// /// Indicates the service account and the API key belong to different projects. /// CustomTokenMismatch = 11, + /// /// Indicates the saved auth credential is invalid. The user needs to sign in again. /// InvalidUserToken = 12, + /// /// Indicates that the operation is not allowed (provider disabled or email/password not enabled). /// OperationNotAllowed = 13, + /// /// Indicates that the user must reauthenticate due to a recent-login requirement. /// RequiresRecentLogin = 14, + /// /// Indicates that a different user than the current user was used for reauthentication. /// UserMismatch = 15, + /// /// Indicates that the provider has already been linked to the user. /// ProviderAlreadyLinked = 16, + /// /// Indicates the provider is not linked to the user. /// NoSuchProvider = 17, + /// /// Indicates the email asserted by a credential is already in use by another account. /// CredentialAlreadyInUse = 18, + /// /// Indicates that the phone auth credential was created with an empty verification ID. /// MissingVerificationId = 19, + /// /// Indicates that the phone auth credential was created with an empty verification code. /// MissingVerificationCode = 20, + /// /// Indicates that the phone auth credential was created with an invalid verification code. /// InvalidVerificationCode = 21, + /// /// Indicates that the phone auth credential was created with an invalid verification ID. /// InvalidVerificationId = 22, + /// /// Indicates that the SMS code has expired. /// SessionExpired = 23, + /// /// Indicates that the quota of SMS messages for a given project has been exceeded. /// QuotaExceeded = 24, + /// /// Indicates that the APNs device token was not obtained or forwarded. /// MissingAppToken = 25, + /// /// Indicates that the APNs device token was missing when required for phone auth. /// MissingAppCredential = 26, + /// /// Indicates that an invalid APNs device token was used. /// InvalidAppCredential = 27, + /// /// Indicates that a notification was not forwarded to Firebase Auth when required. /// NotificationNotForwarded = 28, + /// /// Indicates an invalid recipient email was sent in the request. /// InvalidRecipientEmail = 29, + /// /// Indicates an invalid sender email is set in the console for this action. /// InvalidSender = 30, + /// /// Indicates an invalid email template for sending update email. /// InvalidMessagePayload = 31, + /// /// Indicates that the iOS bundle ID is missing when required. /// MissingIosBundleId = 32, + /// /// Indicates that the Android package name is missing when required. /// MissingAndroidPackageName = 33, + /// /// Indicates that the domain specified in the continue URL is not allowlisted. /// UnauthorizedDomain = 34, + /// /// Indicates that the domain specified in the continue URL is not valid. /// InvalidContinueUri = 35, + /// /// Indicates an invalid API key was supplied in the request. /// InvalidApiKey = 36, + /// /// Indicates the app is not authorized to use Firebase Authentication with the provided API key. /// AppNotAuthorized = 37, + /// /// Indicates an error occurred when accessing the keychain. /// KeychainError = 38, + /// /// Indicates an internal error occurred. /// InternalError = 39, + /// /// Indicates a network error occurred during the operation. /// NetworkError = 40, + /// /// Indicates the request has been blocked after an abnormal number of requests. /// TooManyRequests = 41, + /// /// Indicates a web-based sign-in flow failed due to a web context network error. /// - WebNetworkRequestFailed = 42 + WebNetworkRequestFailed = 42, } +/// +/// Exception representing an authentication failure with a normalized reason. +/// public class FirebaseAuthException : FirebaseException { + /// + /// Normalized authentication error reason. + /// public FIRAuthError Reason { get; } + + /// + /// Gets the error code string that provides additional information about the error. + /// public string? ErrorCode { get; } + + /// + /// Native error domain, when available. + /// public string? NativeErrorDomain { get; } + + /// + /// Native error code, when available. + /// public long? NativeErrorCode { get; } + + /// + /// Email address involved in the failure, when available. + /// public string? Email { get; } + /// + /// Creates a new instance. + /// + /// Normalized authentication error reason. public FirebaseAuthException(FIRAuthError reason) - : this(reason, string.Empty) - { - } + : this(reason, string.Empty) { } + /// + /// Creates a new instance. + /// + /// Normalized authentication error reason. + /// Error message. public FirebaseAuthException(FIRAuthError reason, string message) - : this(reason, message, null, null, null, null, null) - { - } + : this(reason, message, null, null, null, null, null) { } + /// + /// Creates a new instance. + /// + /// Normalized authentication error reason. + /// Error message. + /// Inner exception. public FirebaseAuthException(FIRAuthError reason, string message, Exception inner) - : this(reason, message, inner, null, null, null, null) - { - } + : this(reason, message, inner, null, null, null, null) { } + /// + /// Creates a new instance. + /// + /// Normalized authentication error reason. + /// Error message. + /// Inner exception. + /// Native/provider error code string, when available. + /// Native error domain, when available. + /// Native error code, when available. + /// Email involved in the failure, when available. public FirebaseAuthException( FIRAuthError reason, string message, @@ -208,7 +294,8 @@ public FirebaseAuthException( string? errorCode, string? nativeErrorDomain, long? nativeErrorCode, - string? email) + string? email + ) : base(message, inner) { Reason = reason; @@ -218,13 +305,24 @@ public FirebaseAuthException( Email = email; } + /// + /// Creates a from a provider/native error code and related metadata. + /// + /// Native/provider error code string, when available. + /// Error message. + /// Inner exception. + /// Native error domain, when available. + /// Native error code, when available. + /// Email involved in the failure, when available. + /// A new . public static FirebaseAuthException FromErrorCode( string? errorCode, string? message, Exception? inner = null, string? nativeErrorDomain = null, long? nativeErrorCode = null, - string? email = null) + string? email = null + ) { var reason = MapReason(errorCode); return new FirebaseAuthException( @@ -234,7 +332,8 @@ public static FirebaseAuthException FromErrorCode( errorCode, nativeErrorDomain, nativeErrorCode, - email); + email + ); } private static FIRAuthError MapReason(string? errorCode) @@ -252,8 +351,10 @@ private static FIRAuthError MapReason(string? errorCode) return FIRAuthError.NetworkError; } - if(Enum.TryParse(normalized, ignoreCase: true, out FIRAuthError reason) - && Enum.IsDefined(typeof(FIRAuthError), reason)) { + if( + Enum.TryParse(normalized, ignoreCase: true, out FIRAuthError reason) + && Enum.IsDefined(typeof(FIRAuthError), reason) + ) { return reason; } @@ -285,7 +386,12 @@ private static string NormalizeErrorCode(string errorCode) private static bool IsNumericCode(string code) { - return long.TryParse(code, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _); + return long.TryParse( + code, + System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, + out _ + ); } private static string ToPascalCase(string code) diff --git a/src/Core/Shared/Exceptions/FirebaseException.cs b/src/Core/Shared/Exceptions/FirebaseException.cs index 94263671..80242887 100644 --- a/src/Core/Shared/Exceptions/FirebaseException.cs +++ b/src/Core/Shared/Exceptions/FirebaseException.cs @@ -1,16 +1,31 @@ namespace Plugin.Firebase.Core.Exceptions; +/// +/// Base exception type for Plugin.Firebase. +/// public class FirebaseException : Exception { + /// + /// Creates a new instance. + /// public FirebaseException() { } + /// + /// Creates a new instance with a message. + /// + /// Error message. public FirebaseException(string message) : base(message) { } + /// + /// Creates a new instance with a message and an inner exception. + /// + /// Error message. + /// Inner exception. public FirebaseException(string message, Exception? inner) : base(message, inner) { diff --git a/src/Core/Shared/Extensions/EnumerableExtensions.cs b/src/Core/Shared/Extensions/EnumerableExtensions.cs index b0877530..3febf5d6 100644 --- a/src/Core/Shared/Extensions/EnumerableExtensions.cs +++ b/src/Core/Shared/Extensions/EnumerableExtensions.cs @@ -2,8 +2,19 @@ namespace Plugin.Firebase.Core.Extensions; +/// +/// Extensions for working with . +/// public static class EnumerableExtensions { + /// + /// Null-safe sequence equality comparison. + /// + /// Element type. + /// First sequence. + /// Second sequence. + /// Optional element equality comparer. + /// true if both sequences are equal; otherwise false. public static bool SequenceEqualSafe(this IEnumerable? @this, IEnumerable? other, Func? comparer = null) { if(@this == null && other == null) { @@ -17,6 +28,14 @@ public static bool SequenceEqualSafe(this IEnumerable? @this, IEnumerable< } } + /// + /// Sequence equality comparison using an optional element comparer. + /// + /// Element type. + /// First sequence. + /// Second sequence. + /// Optional element equality comparer. + /// true if both sequences are equal; otherwise false. public static bool SequenceEqual(this IEnumerable source, IEnumerable other, Func? comparer) { return comparer == null ? source.SequenceEqual(other) : source.SequenceEqual(other, new FuncEqualityComparer(comparer)); diff --git a/src/Core/Shared/Utils/FuncEqualityComparer.cs b/src/Core/Shared/Utils/FuncEqualityComparer.cs index 0bb445bd..a2e5dbb3 100644 --- a/src/Core/Shared/Utils/FuncEqualityComparer.cs +++ b/src/Core/Shared/Utils/FuncEqualityComparer.cs @@ -1,21 +1,35 @@ namespace Plugin.Firebase.Core.Utils; +/// +/// implementation backed by delegate(s). +/// +/// Element type. public class FuncEqualityComparer : IEqualityComparer { private readonly Func _comparer; private readonly Func _hash; + /// + /// Creates a new instance using the provided equality comparer and a constant hash code. + /// + /// Equality comparer. public FuncEqualityComparer(Func comparer) : this(comparer, _ => 0) { } + /// + /// Creates a new instance using the provided equality comparer and hash function. + /// + /// Equality comparer. + /// Hash function. public FuncEqualityComparer(Func comparer, Func hash) { _comparer = comparer; _hash = hash; } + /// public bool Equals(T? x, T? y) { if(x is null && y is null) { @@ -28,6 +42,7 @@ public bool Equals(T? x, T? y) return _comparer(x, y); } + /// public int GetHashCode(T obj) { return _hash(obj); diff --git a/src/Crashlytics/Crashlytics.csproj b/src/Crashlytics/Crashlytics.csproj index 4a8858a9..f6de8007 100644 --- a/src/Crashlytics/Crashlytics.csproj +++ b/src/Crashlytics/Crashlytics.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.Crashlytics - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase @@ -42,6 +42,10 @@ default + + + false + diff --git a/src/Crashlytics/Platforms/iOS/FirebaseCrashlyticsImplementation.cs b/src/Crashlytics/Platforms/iOS/FirebaseCrashlyticsImplementation.cs index d38390cb..e905be6c 100644 --- a/src/Crashlytics/Platforms/iOS/FirebaseCrashlyticsImplementation.cs +++ b/src/Crashlytics/Platforms/iOS/FirebaseCrashlyticsImplementation.cs @@ -4,86 +4,110 @@ namespace Plugin.Firebase.Crashlytics; +/// +/// iOS implementation of that wraps the native Firebase Crashlytics SDK. +/// public sealed class FirebaseCrashlyticsImplementation : DisposableBase, IFirebaseCrashlytics { private readonly FirebaseCrashlytics _instance; + /// + /// Initializes a new instance of the class. + /// public FirebaseCrashlyticsImplementation() { _instance = FirebaseCrashlytics.SharedInstance; } + /// public void SetCrashlyticsCollectionEnabled(bool enabled) { _instance.SetCrashlyticsCollectionEnabled(enabled); } + /// public void SetCustomKey(string key, bool value) { _instance.SetCustomValue(new NSNumber(value), key); } + /// public void SetCustomKey(string key, int value) { _instance.SetCustomValue(new NSNumber(value), key); } + /// public void SetCustomKey(string key, long value) { _instance.SetCustomValue(new NSNumber(value), key); } + /// public void SetCustomKey(string key, float value) { _instance.SetCustomValue(new NSNumber(value), key); } + /// public void SetCustomKey(string key, double value) { _instance.SetCustomValue(new NSNumber(value), key); } + /// public void SetCustomKey(string key, string value) { _instance.SetCustomValue(new NSString(value), key); } + /// public void SetCustomKeys(IDictionary customKeysAndValues) { - var nsDict = NSDictionary.FromObjectsAndKeys(customKeysAndValues.Values.ToArray(), customKeysAndValues.Keys.ToArray()); + var nsDict = NSDictionary.FromObjectsAndKeys( + customKeysAndValues.Values.ToArray(), + customKeysAndValues.Keys.ToArray() + ); _instance.SetCustomKeysAndValues(nsDict); } + /// public void SetUserId(string identifier) { _instance.SetUserId(identifier); } + /// public void Log(string message) { _instance.Log(message); } + /// public void RecordException(Exception exception) { _instance.RecordExceptionModel(CrashlyticsException.Create(exception)); } + /// public bool DidCrashOnPreviousExecution() { return _instance.DidCrashDuringPreviousExecution; } + /// public Task CheckForUnsentReportsAsync() { return _instance.CheckForUnsentReportsAsync(); } + /// public void SendUnsentReports() { _instance.SendUnsentReports(); } + /// public void DeleteUnsentReports() { _instance.DeleteUnsentReports(); diff --git a/src/Crashlytics/Shared/CrossFirebaseCrashlytics.cs b/src/Crashlytics/Shared/CrossFirebaseCrashlytics.cs index 5a09976f..ac6cabf1 100644 --- a/src/Crashlytics/Shared/CrossFirebaseCrashlytics.cs +++ b/src/Crashlytics/Shared/CrossFirebaseCrashlytics.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.Crashlytics; +/// +/// Cross-platform entry point for Firebase Crashlytics. +/// public sealed class CrossFirebaseCrashlytics { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseCrashlytics CreateInstance() { @@ -34,16 +40,21 @@ public static IFirebaseCrashlytics Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/Firestore/Firestore.csproj b/src/Firestore/Firestore.csproj index 40c63960..97f9f375 100644 --- a/src/Firestore/Firestore.csproj +++ b/src/Firestore/Firestore.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.Firestore - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/Firestore/Platforms/iOS/CollectionReferenceWrapper.cs b/src/Firestore/Platforms/iOS/CollectionReferenceWrapper.cs index cc983d7b..192bff3b 100644 --- a/src/Firestore/Platforms/iOS/CollectionReferenceWrapper.cs +++ b/src/Firestore/Platforms/iOS/CollectionReferenceWrapper.cs @@ -4,39 +4,53 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore collection reference. +/// public sealed class CollectionReferenceWrapper : QueryWrapper, ICollectionReference { private readonly CollectionReference _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS collection reference to wrap. public CollectionReferenceWrapper(CollectionReference reference) : base(reference) { _wrapped = reference; } + /// public IDocumentReference GetDocument(string documentPath) { return new DocumentReferenceWrapper(_wrapped.GetDocument(documentPath)); } + /// public IDocumentReference CreateDocument() { return new DocumentReferenceWrapper(_wrapped.CreateDocument()); } + /// public Task AddDocumentAsync(object data) { var tcs = new TaskCompletionSource(); DocumentReference documentReference = null; - documentReference = _wrapped.AddDocument(data.ToDictionary(), error => { - if(error == null) { - tcs.SetResult(new DocumentReferenceWrapper(documentReference)); - } else { - tcs.SetException(new FirebaseException(error.LocalizedDescription)); + documentReference = _wrapped.AddDocument( + data.ToDictionary(), + error => { + if(error == null) { + tcs.SetResult(new DocumentReferenceWrapper(documentReference)); + } else { + tcs.SetException(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return tcs.Task; } + /// public IDocumentReference Parent => _wrapped.Parent?.ToAbstract(); -} +} \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/DocumentReferenceWrapper.cs b/src/Firestore/Platforms/iOS/DocumentReferenceWrapper.cs index 439ba46c..962c8fbd 100644 --- a/src/Firestore/Platforms/iOS/DocumentReferenceWrapper.cs +++ b/src/Firestore/Platforms/iOS/DocumentReferenceWrapper.cs @@ -6,18 +6,27 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore document reference. +/// public sealed class DocumentReferenceWrapper : IDocumentReference { + /// + /// Initializes a new instance of the class. + /// + /// The native iOS document reference to wrap. public DocumentReferenceWrapper(DocumentReference reference) { Wrapped = reference; } + /// public Task SetDataAsync(object data, SetOptions options = null) { return SetDataAsync(data.ToDictionary(), options); } + /// public Task SetDataAsync(Dictionary data, SetOptions options) { var nsData = data.ToNSObjectDictionary(); @@ -28,7 +37,10 @@ public Task SetDataAsync(Dictionary data, SetOptions options) case SetOptions.TypeMerge: return Wrapped.SetDataAsync(nsData, true); case SetOptions.TypeMergeFieldPaths: - return Wrapped.SetDataAsync(nsData, options.FieldPaths.Select(x => new NativeFieldPath(x.ToArray())).ToArray()); + return Wrapped.SetDataAsync( + nsData, + options.FieldPaths.Select(x => new NativeFieldPath(x.ToArray())).ToArray() + ); case SetOptions.TypeMergeFields: return Wrapped.SetDataAsync(nsData, options.Fields.ToArray()); default: @@ -36,66 +48,91 @@ public Task SetDataAsync(Dictionary data, SetOptions options) } } + /// public Task SetDataAsync(params (object, object)[] data) { return SetDataAsync(data.ToDictionary()); } + /// public Task SetDataAsync(SetOptions options, params (object, object)[] data) { return SetDataAsync(data.ToDictionary(), options); } + /// public Task UpdateDataAsync(Dictionary data) { return Wrapped.UpdateDataAsync(data.ToNSObjectDictionary()); } + /// public Task UpdateDataAsync(params (string, object)[] data) { return Wrapped.UpdateDataAsync(data.ToNSObjectDictionary()); } + /// public Task DeleteDocumentAsync() { return Wrapped.DeleteDocumentAsync(); } + /// public Task> GetDocumentSnapshotAsync(Source source = Source.Default) { var tcs = new TaskCompletionSource>(); - Wrapped.GetDocument(source.ToNative(), (snapshot, error) => { - if(error == null) { - tcs.SetResult(snapshot.ToAbstract()); - } else { - tcs.SetException(new FirebaseException(error.LocalizedDescription)); + Wrapped.GetDocument( + source.ToNative(), + (snapshot, error) => { + if(error == null) { + tcs.SetResult(snapshot.ToAbstract()); + } else { + tcs.SetException(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return tcs.Task; } + /// public IDisposable AddSnapshotListener( Action> onChanged, Action onError = null, - bool includeMetaDataChanges = false) + bool includeMetaDataChanges = false + ) { - var registration = Wrapped.AddSnapshotListener(includeMetaDataChanges, (snapshot, error) => { - if(error == null) { - onChanged(snapshot.ToAbstract()); - } else { - onError?.Invoke(new FirebaseException(error.LocalizedDescription)); + var registration = Wrapped.AddSnapshotListener( + includeMetaDataChanges, + (snapshot, error) => { + if(error == null) { + onChanged(snapshot.ToAbstract()); + } else { + onError?.Invoke(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return new DisposableWithAction(registration.Remove); } + /// public ICollectionReference GetCollection(string collectionPath) { return Wrapped.GetCollection(collectionPath).ToAbstract(); } + /// public string Id => Wrapped.Id; + + /// public string Path => Wrapped.Path; - public ICollectionReference Parent => Wrapped.Parent == null ? null : new CollectionReferenceWrapper(Wrapped.Parent); + + /// + public ICollectionReference Parent => + Wrapped.Parent == null ? null : new CollectionReferenceWrapper(Wrapped.Parent); + + /// + /// Gets the underlying native iOS document reference. + /// public DocumentReference Wrapped { get; } } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/DocumentSnapshotWrapper.cs b/src/Firestore/Platforms/iOS/DocumentSnapshotWrapper.cs index 6e41d4e3..748f1f45 100644 --- a/src/Firestore/Platforms/iOS/DocumentSnapshotWrapper.cs +++ b/src/Firestore/Platforms/iOS/DocumentSnapshotWrapper.cs @@ -3,25 +3,48 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore document snapshot with typed data. +/// +/// The type to deserialize the document data into. public sealed class DocumentSnapshotWrapper : DocumentSnapshotWrapper, IDocumentSnapshot { + /// + /// Initializes a new instance of the class. + /// + /// The native iOS document snapshot to wrap. public DocumentSnapshotWrapper(DocumentSnapshot documentSnapshot) - : base(documentSnapshot) - { - } + : base(documentSnapshot) { } + /// public new T Data => Wrapped.Data == null ? default(T) : Wrapped.Data.Cast(Wrapped.Id); } +/// +/// Wraps a native iOS Firestore document snapshot. +/// public class DocumentSnapshotWrapper : IDocumentSnapshot { + /// + /// Initializes a new instance of the class. + /// + /// The native iOS document snapshot to wrap. public DocumentSnapshotWrapper(DocumentSnapshot snapshot) { Wrapped = snapshot; } + /// public object Data => Wrapped.Data; + + /// public ISnapshotMetadata Metadata => Wrapped.Metadata.ToAbstract(); + + /// public IDocumentReference Reference => Wrapped.Reference.ToAbstract(); + + /// + /// Gets the underlying native iOS document snapshot. + /// public DocumentSnapshot Wrapped { get; } } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/Extensions/DateExtensions.cs b/src/Firestore/Platforms/iOS/Extensions/DateExtensions.cs index 6d001eb4..2e56535e 100644 --- a/src/Firestore/Platforms/iOS/Extensions/DateExtensions.cs +++ b/src/Firestore/Platforms/iOS/Extensions/DateExtensions.cs @@ -2,15 +2,32 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS.Extensions; +/// +/// Extension methods for converting Firestore timestamp types to .NET date types. +/// public static class DateExtensions { + /// + /// Converts a native iOS Firestore timestamp to a . + /// + /// The native timestamp to convert. + /// A representation of the timestamp, or default if null. public static DateTimeOffset ToDateTimeOffset(this Timestamp @this) { - return @this == null ? default(DateTimeOffset) : DateTime.SpecifyKind((DateTime) @this.DateValue, DateTimeKind.Utc); + return @this == null + ? default(DateTimeOffset) + : DateTime.SpecifyKind((DateTime) @this.DateValue, DateTimeKind.Utc); } + /// + /// Converts a native iOS Firestore timestamp to a . + /// + /// The native timestamp to convert. + /// A representation of the timestamp in UTC, or default if null. public static DateTime ToDateTime(this Timestamp @this) { - return @this == null ? default(DateTime) : DateTime.SpecifyKind((DateTime) @this.DateValue, DateTimeKind.Utc); + return @this == null + ? default(DateTime) + : DateTime.SpecifyKind((DateTime) @this.DateValue, DateTimeKind.Utc); } } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/Extensions/DictionaryExtensions.cs b/src/Firestore/Platforms/iOS/Extensions/DictionaryExtensions.cs index de5d9977..5c52383b 100644 --- a/src/Firestore/Platforms/iOS/Extensions/DictionaryExtensions.cs +++ b/src/Firestore/Platforms/iOS/Extensions/DictionaryExtensions.cs @@ -3,26 +3,43 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS.Extensions; +/// +/// Extension methods for converting between .NET dictionaries and native iOS NSDictionary types. +/// public static class DictionaryExtensions { - public static NSDictionary ToNSDictionaryFromNonGeneric(this IDictionary dictionary) + /// + /// Converts a non-generic to a native iOS NSDictionary. + /// + /// The dictionary to convert. + /// A native iOS NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionaryFromNonGeneric( + this IDictionary dictionary + ) { if(dictionary.Count > 0) { var nsDictionary = new NSMutableDictionary(); foreach(DictionaryEntry entry in dictionary) { - PutIntoNSDictionary(new KeyValuePair(entry.Key.ToString(), entry.Value), ref nsDictionary); + PutIntoNSDictionary( + new KeyValuePair(entry.Key.ToString(), entry.Value), + ref nsDictionary + ); } return NSDictionary.FromObjectsAndKeys( nsDictionary.Values.ToArray(), nsDictionary.Keys.ToArray(), - (nint) nsDictionary.Count); + (nint) nsDictionary.Count + ); } else { return new NSDictionary(); } } - private static void PutIntoNSDictionary(KeyValuePair pair, ref NSMutableDictionary nsDictionary) + private static void PutIntoNSDictionary( + KeyValuePair pair, + ref NSMutableDictionary nsDictionary + ) { switch(pair.Value) { case bool x: @@ -57,11 +74,18 @@ private static void PutIntoNSDictionary(KeyValuePair pair, ref N nsDictionary.Add((NSString) pair.Key, new NSNull()); break; } else { - throw new ArgumentException($"Couldn't put object of type {pair.Value.GetType()} into NSDictionary"); + throw new ArgumentException( + $"Couldn't put object of type {pair.Value.GetType()} into NSDictionary" + ); } } } + /// + /// Converts an object to a dictionary using Firestore property attributes. + /// + /// The object to convert. + /// A dictionary with property names as keys and their values. public static Dictionary ToDictionary(this object @this) { var dict = new Dictionary(); @@ -78,7 +102,10 @@ public static Dictionary ToDictionary(this object @this) } } - var timestampAttributes = property.GetCustomAttributes(typeof(FirestoreServerTimestampAttribute), true); + var timestampAttributes = property.GetCustomAttributes( + typeof(FirestoreServerTimestampAttribute), + true + ); if(timestampAttributes.Any()) { var attribute = (FirestoreServerTimestampAttribute) timestampAttributes[0]; dict[attribute.PropertyName] = NativeFieldValue.ServerTimestamp; @@ -87,11 +114,23 @@ public static Dictionary ToDictionary(this object @this) return dict; } + /// + /// Converts a native iOS NSDictionary to a .NET object of the specified type. + /// + /// The NSDictionary to convert. + /// The target type to convert to. + /// The converted object. public static object ToDictionaryObject(this NSDictionary @this, Type targetType) { if(targetType == null) { return @this.ToDictionary(); - } else if(targetType.IsGenericType && (targetType.GetGenericTypeDefinition() == typeof(IDictionary<,>) || targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>))) { + } else if( + targetType.IsGenericType + && ( + targetType.GetGenericTypeDefinition() == typeof(IDictionary<,>) + || targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>) + ) + ) { var types = targetType.GenericTypeArguments; return @this.ToDictionary(types[0], types[1]); } else { @@ -99,21 +138,43 @@ public static object ToDictionaryObject(this NSDictionary @this, Type targetType } } + /// + /// Converts a native iOS NSDictionary to a typed .NET dictionary. + /// + /// The NSDictionary to convert. + /// The type for dictionary keys. + /// The type for dictionary values. + /// A typed dictionary containing the converted key-value pairs. public static IDictionary ToDictionary(this NSDictionary @this, Type keyType, Type valueType) { - var dict = (IDictionary) Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType)); + var dict = (IDictionary) + Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType)); foreach(var pair in @this) { dict[pair.Key.ToObject(keyType)] = pair.Value.ToObject(valueType); } return dict; } - public static Dictionary ToNSObjectDictionary(this Dictionary @this) + /// + /// Converts dictionary values to native iOS NSObject types. + /// + /// The dictionary to convert. + /// A dictionary with values converted to NSObject types. + public static Dictionary ToNSObjectDictionary( + this Dictionary @this + ) { return @this.ToDictionary(x => x.Key, x => (object) x.Value.ToNSObject()); } - public static Dictionary ToNSObjectDictionary(this IEnumerable<(string, object)> @this) + /// + /// Converts a collection of key-value tuples to a dictionary with NSObject values. + /// + /// The collection of tuples to convert. + /// A dictionary with NSObject values. + public static Dictionary ToNSObjectDictionary( + this IEnumerable<(string, object)> @this + ) { var dict = new Dictionary(); foreach(var (key, value) in @this) { diff --git a/src/Firestore/Platforms/iOS/Extensions/FirestoreExtensions.cs b/src/Firestore/Platforms/iOS/Extensions/FirestoreExtensions.cs index bb470875..4195e5ec 100644 --- a/src/Firestore/Platforms/iOS/Extensions/FirestoreExtensions.cs +++ b/src/Firestore/Platforms/iOS/Extensions/FirestoreExtensions.cs @@ -1,56 +1,104 @@ using Firebase.CloudFirestore; using Plugin.Firebase.Core.Exceptions; -using NativeFirestoreSettings = Firebase.CloudFirestore.FirestoreSettings; -using NativeFieldValue = Firebase.CloudFirestore.FieldValue; using NativeDocumentChange = Firebase.CloudFirestore.DocumentChange; using NativeDocumentChangeType = Firebase.CloudFirestore.DocumentChangeType; -using NativeSource = Firebase.CloudFirestore.FirestoreSource; using NativeFieldPath = Firebase.CloudFirestore.FieldPath; +using NativeFieldValue = Firebase.CloudFirestore.FieldValue; +using NativeFirestoreSettings = Firebase.CloudFirestore.FirestoreSettings; +using NativeSource = Firebase.CloudFirestore.FirestoreSource; namespace Plugin.Firebase.Firestore.Platforms.iOS.Extensions { + /// + /// Extension methods for converting between native iOS Firestore types and abstract wrapper types. + /// public static class FirestoreExtensions { + /// + /// Converts a native iOS document snapshot to an abstract wrapper. + /// + /// The native document snapshot. + /// An abstract document snapshot wrapper. public static IDocumentSnapshot ToAbstract(this DocumentSnapshot @this) { return new DocumentSnapshotWrapper(@this); } + /// + /// Converts a native iOS document snapshot to a typed abstract wrapper. + /// + /// The type to deserialize the document data into. + /// The native document snapshot. + /// A typed abstract document snapshot wrapper. public static IDocumentSnapshot ToAbstract(this DocumentSnapshot @this) { return new DocumentSnapshotWrapper(@this); } + /// + /// Converts an abstract document reference to the native iOS type. + /// + /// The abstract document reference. + /// The native iOS document reference. + /// Thrown if the implementation is not supported. public static DocumentReference ToNative(this IDocumentReference @this) { if(@this is DocumentReferenceWrapper wrapper) { return wrapper.Wrapped; } - throw new FirebaseException($"This implementation of {nameof(IDocumentReference)} is not supported for this method"); + throw new FirebaseException( + $"This implementation of {nameof(IDocumentReference)} is not supported for this method" + ); } + /// + /// Converts a native iOS document reference to an abstract wrapper. + /// + /// The native document reference. + /// An abstract document reference wrapper. public static IDocumentReference ToAbstract(this DocumentReference @this) { return new DocumentReferenceWrapper(@this); } + /// + /// Converts an abstract document snapshot to the native iOS type. + /// + /// The abstract document snapshot. + /// The native iOS document snapshot. + /// Thrown if the implementation is not supported. public static DocumentSnapshot ToNative(this IDocumentSnapshot @this) { if(@this is DocumentSnapshotWrapper wrapper) { return wrapper.Wrapped; } - throw new FirebaseException($"This implementation of {nameof(IDocumentSnapshot)} is not supported for this method"); + throw new FirebaseException( + $"This implementation of {nameof(IDocumentSnapshot)} is not supported for this method" + ); } + /// + /// Converts a native iOS document change to an abstract typed document change. + /// + /// The type to deserialize the document data into. + /// The native document change. + /// A typed abstract document change. public static DocumentChange ToAbstract(this NativeDocumentChange @this) { return new DocumentChange( @this.Document.ToAbstract(), @this.Type.ToAbstract(), (int) @this.NewIndex, - (int) @this.OldIndex); + (int) @this.OldIndex + ); } + /// + /// Converts a native iOS document change type to the abstract enum. + /// + /// The native document change type. + /// The abstract document change type. + /// Thrown if the type is unknown. public static DocumentChangeType ToAbstract(this NativeDocumentChangeType @this) { switch(@this) { @@ -61,37 +109,69 @@ public static DocumentChangeType ToAbstract(this NativeDocumentChangeType @this) case NativeDocumentChangeType.Removed: return DocumentChangeType.Removed; default: - throw new FirebaseException($"Couldn't convert {@this} to abstract {nameof(DocumentChangeType)}"); + throw new FirebaseException( + $"Couldn't convert {@this} to abstract {nameof(DocumentChangeType)}" + ); } } + /// + /// Converts a native iOS query to an abstract wrapper. + /// + /// The native query. + /// An abstract query wrapper. public static IQuery ToAbstract(this Query @this) { return new QueryWrapper(@this); } + /// + /// Converts native iOS snapshot metadata to an abstract wrapper. + /// + /// The native snapshot metadata. + /// An abstract snapshot metadata wrapper. public static ISnapshotMetadata ToAbstract(this SnapshotMetadata @this) { return new SnapshotMetadataWrapper(@this); } + /// + /// Converts a native iOS transaction to an abstract wrapper. + /// + /// The native transaction. + /// An abstract transaction wrapper. public static ITransaction ToAbstract(this Transaction @this) { return new TransactionWrapper(@this); } + /// + /// Converts a native iOS write batch to an abstract wrapper. + /// + /// The native write batch. + /// An abstract write batch wrapper. public static IWriteBatch ToAbstract(this WriteBatch @this) { return new WriteBatchWrapper(@this); } + /// + /// Converts an abstract field value to the native iOS type. + /// + /// The abstract field value. + /// The native iOS field value. + /// Thrown if the field value type is unknown. public static NativeFieldValue ToNative(this FieldValue @this) { switch(@this.Type) { case FieldValueType.ArrayUnion: - return NativeFieldValue.FromArrayUnion(@this.Elements.Select(x => x.ToNSObject()).ToArray()); + return NativeFieldValue.FromArrayUnion( + @this.Elements.Select(x => x.ToNSObject()).ToArray() + ); case FieldValueType.ArrayRemove: - return NativeFieldValue.FromArrayRemove(@this.Elements.Select(x => x.ToNSObject()).ToArray()); + return NativeFieldValue.FromArrayRemove( + @this.Elements.Select(x => x.ToNSObject()).ToArray() + ); case FieldValueType.IntegerIncrement: return NativeFieldValue.FromIntegerIncrement((long) @this.IncrementValue); case FieldValueType.DoubleIncrement: @@ -101,22 +181,39 @@ public static NativeFieldValue ToNative(this FieldValue @this) case FieldValueType.ServerTimestamp: return NativeFieldValue.ServerTimestamp; } - throw new ArgumentException($"Couldn't convert FieldValue to native because of unknown type: {@this.Type}"); + throw new ArgumentException( + $"Couldn't convert FieldValue to native because of unknown type: {@this.Type}" + ); } + /// + /// Converts native iOS Firestore settings to abstract settings. + /// + /// The native Firestore settings. + /// Abstract Firestore settings. public static FirestoreSettings ToAbstract(this NativeFirestoreSettings @this) { return new FirestoreSettings(@this.Host, @this.SslEnabled); } + /// + /// Converts abstract Firestore settings to the native iOS type. + /// + /// The abstract Firestore settings. + /// Native iOS Firestore settings. public static NativeFirestoreSettings ToNative(this FirestoreSettings @this) { return new NativeFirestoreSettings { Host = @this.Host, - SslEnabled = @this.IsSslEnabled + SslEnabled = @this.IsSslEnabled, }; } + /// + /// Converts an abstract data source to the native iOS type. + /// + /// The abstract source. + /// The native iOS Firestore source. public static NativeSource ToNative(this Source @this) { switch(@this) { @@ -129,11 +226,23 @@ public static NativeSource ToNative(this Source @this) } } + /// + /// Converts an abstract field path to the native iOS type. + /// + /// The abstract field path. + /// The native iOS field path. public static NativeFieldPath ToNative(this FieldPath @this) { - return @this.IsDocumentId ? NativeFieldPath.GetDocumentId() : new NativeFieldPath(@this.Fields); + return @this.IsDocumentId + ? NativeFieldPath.GetDocumentId() + : new NativeFieldPath(@this.Fields); } + /// + /// Converts a native iOS collection reference to an abstract wrapper. + /// + /// The native collection reference. + /// An abstract collection reference wrapper. public static ICollectionReference ToAbstract(this CollectionReference @this) { return new CollectionReferenceWrapper(@this); diff --git a/src/Firestore/Platforms/iOS/Extensions/ListExtensions.cs b/src/Firestore/Platforms/iOS/Extensions/ListExtensions.cs index 78f66c69..d8bc8b56 100644 --- a/src/Firestore/Platforms/iOS/Extensions/ListExtensions.cs +++ b/src/Firestore/Platforms/iOS/Extensions/ListExtensions.cs @@ -2,8 +2,16 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS.Extensions; +/// +/// Extension methods for converting between .NET lists and native iOS NSArray types. +/// public static class ListExtensions { + /// + /// Converts a .NET list to a native iOS NSArray. + /// + /// The list to convert. + /// A native iOS NSArray containing the converted elements. public static NSArray ToNSArray(this IList @this) { var array = new NSMutableArray(); @@ -13,11 +21,16 @@ public static NSArray ToNSArray(this IList @this) return array; } + /// + /// Converts a native iOS NSArray to a typed .NET list. + /// + /// The NSArray to convert. + /// The element type for the resulting list. + /// A typed list containing the converted elements. public static IList ToList(this NSArray @this, Type targetType) { var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(targetType)); for(nuint i = 0; i < @this.Count; i++) { - var item = @this.GetItem(i); if(item != null) list.Add(item.ToObject(targetType)); diff --git a/src/Firestore/Platforms/iOS/Extensions/NSObjectExtensions.cs b/src/Firestore/Platforms/iOS/Extensions/NSObjectExtensions.cs index 26147946..e0fb7bb7 100644 --- a/src/Firestore/Platforms/iOS/Extensions/NSObjectExtensions.cs +++ b/src/Firestore/Platforms/iOS/Extensions/NSObjectExtensions.cs @@ -6,19 +6,39 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS.Extensions; +/// +/// Extension methods for converting between .NET objects and native iOS NSObject types. +/// public static class NSObjectExtensions { + /// + /// Casts a native iOS NSDictionary to a typed .NET object. + /// + /// The target type to cast to. + /// The NSDictionary to cast. + /// Optional document ID to set on the object. + /// The typed object. public static T Cast(this NSDictionary @this, string documentId = null) { return (T) @this.Cast(typeof(T), documentId); } + /// + /// Casts a native iOS NSDictionary to a .NET object of the specified type. + /// + /// The NSDictionary to cast. + /// The target type to cast to. + /// Optional document ID to set on the object. + /// The converted object. public static object Cast(this NSDictionary @this, Type targetType, string documentId = null) { var instance = Activator.CreateInstance(targetType); var properties = targetType.GetProperties(); foreach(var property in properties) { - if(documentId != null && property.GetCustomAttributes(typeof(FirestoreDocumentIdAttribute), true).Any()) { + if( + documentId != null + && property.GetCustomAttributes(typeof(FirestoreDocumentIdAttribute), true).Any() + ) { property.SetValue(instance, documentId); continue; } @@ -28,28 +48,47 @@ public static object Cast(this NSDictionary @this, Type targetType, string docum var attribute = (FirestorePropertyAttribute) attributes[0]; var value = @this[attribute.PropertyName]; if(value == null) { - Debug.WriteLine($"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because it's not contained in the NSDictionary."); + Debug.WriteLine( + $"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because it's not contained in the NSDictionary." + ); } else { property.SetValue(instance, value.ToObject(property.PropertyType)); } } - var timestampAttributes = property.GetCustomAttributes(typeof(FirestoreServerTimestampAttribute), true); + var timestampAttributes = property.GetCustomAttributes( + typeof(FirestoreServerTimestampAttribute), + true + ); if(timestampAttributes.Any()) { var attribute = (FirestoreServerTimestampAttribute) timestampAttributes[0]; var value = @this[attribute.PropertyName]; if(value == null) { - Debug.WriteLine($"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because value is null"); - } else if(property.PropertyType == typeof(DateTimeOffset) && value is Timestamp timestamp) { + Debug.WriteLine( + $"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because value is null" + ); + } else if( + property.PropertyType == typeof(DateTimeOffset) + && value is Timestamp timestamp + ) { property.SetValue(instance, timestamp.ToDateTimeOffset()); } else { - Debug.WriteLine($"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because properties that use the {nameof(FirestoreServerTimestampAttribute)} need to be of type {nameof(DateTimeOffset)} and value of type {nameof(Timestamp)}"); + Debug.WriteLine( + $"[Plugin.Firebase] Couldn't cast property '{attribute.PropertyName}' of '{targetType}' because properties that use the {nameof(FirestoreServerTimestampAttribute)} need to be of type {nameof(DateTimeOffset)} and value of type {nameof(Timestamp)}" + ); } } } return instance; } + /// + /// Converts a native iOS NSObject to a .NET object. + /// + /// The NSObject to convert. + /// Optional target type for the conversion. + /// The converted .NET object. + /// Thrown if the NSObject type cannot be converted. public static object ToObject(this NSObject @this, Type targetType = null) { switch(@this) { @@ -75,10 +114,18 @@ public static object ToObject(this NSObject @this, Type targetType = null) case NSNull: return null; default: - throw new ArgumentException($"Could not convert NSObject of type {@this.GetType()} to object"); + throw new ArgumentException( + $"Could not convert NSObject of type {@this.GetType()} to object" + ); } } + /// + /// Converts a native iOS NSNumber to a .NET object of the appropriate numeric type. + /// + /// The NSNumber to convert. + /// Optional target type for the conversion. + /// The converted numeric value. public static object ToObject(this NSNumber @this, Type targetType = null) { if(targetType == null) { @@ -119,11 +166,19 @@ private static Type GetGenericListType(Type targetType) { var genericType = targetType.GenericTypeArguments.FirstOrDefault(); if(genericType == null) { - throw new ArgumentException($"Couldn't get generic list type of targetType {targetType}. Make sure to use a list IList instead of an array T[] as type in your FirestoreObject."); + throw new ArgumentException( + $"Couldn't get generic list type of targetType {targetType}. Make sure to use a list IList instead of an array T[] as type in your FirestoreObject." + ); } return genericType; } + /// + /// Converts a .NET object to a native iOS NSObject. + /// + /// The object to convert. + /// The native iOS NSObject representation. + /// Thrown if the object type cannot be converted. public static NSObject ToNSObject(this object @this) { switch(@this) { @@ -178,12 +233,19 @@ public static NSObject ToNSObject(this object @this) return new NSNumber(Convert.ToUInt16(@this)); } } - throw new ArgumentException($"Could not convert object of type {@this.GetType()} to NSObject. Does it extend {nameof(IFirestoreObject)}?"); + throw new ArgumentException( + $"Could not convert object of type {@this.GetType()} to NSObject. Does it extend {nameof(IFirestoreObject)}?" + ); } } + /// + /// Converts an to a native iOS NSObject. + /// + /// The Firestore object to convert. + /// The native iOS NSObject representation. public static NSObject ToNSObject(this IFirestoreObject @this) { return @this.ToDictionary().ToNSObject(); } -} +} \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/FirebaseFirestoreImplementation.cs b/src/Firestore/Platforms/iOS/FirebaseFirestoreImplementation.cs index e2c82447..5a8b9dcb 100644 --- a/src/Firestore/Platforms/iOS/FirebaseFirestoreImplementation.cs +++ b/src/Firestore/Platforms/iOS/FirebaseFirestoreImplementation.cs @@ -7,89 +7,110 @@ namespace Plugin.Firebase.Firestore; +/// +/// iOS implementation of that wraps the native Firebase Firestore SDK. +/// public sealed class FirebaseFirestoreImplementation : DisposableBase, IFirebaseFirestore { private FBFirestore _firestore; + /// + /// Initializes a new instance of the class. + /// public FirebaseFirestoreImplementation() { _firestore = FBFirestore.SharedInstance; } - + + /// public IQuery GetCollectionGroup(string collectionId) { return new QueryWrapper(_firestore.GetCollectionGroup(collectionId)); } + /// public ICollectionReference GetCollection(string collectionPath) { return new CollectionReferenceWrapper(_firestore.GetCollection(collectionPath)); } + /// public IDocumentReference GetDocument(string documentPath) { return new DocumentReferenceWrapper(_firestore.GetDocument(documentPath)); } + /// public async Task RunTransactionAsync(Func updateFunc) { FirebaseException exception = null; - var result = await _firestore.RunTransactionAsync((Transaction transaction, ref NSError error) => { - try { - if(error == null) { - return updateFunc(transaction.ToAbstract())?.ToNSObject(); - } else { - exception = new FirebaseException(error.LocalizedDescription); + var result = await _firestore.RunTransactionAsync( + (Transaction transaction, ref NSError error) => { + try { + if(error == null) { + return updateFunc(transaction.ToAbstract())?.ToNSObject(); + } else { + exception = new FirebaseException(error.LocalizedDescription); + } + } catch(Exception e) { + exception = new FirebaseException(e.Message); } - } catch(Exception e) { - exception = new FirebaseException(e.Message); + return null; } - return null; - }); + ); return exception is null ? (TResult) result?.ToObject(typeof(TResult)) : throw exception; } + /// public IWriteBatch CreateBatch() { return _firestore.CreateBatch().ToAbstract(); } + /// public Task WaitForPendingWritesAsync() { return _firestore.WaitForPendingWritesAsync(); } + /// public Task DisableNetworkAsync() { return _firestore.DisableNetworkAsync(); } + /// public Task EnableNetworkAsync() { return _firestore.EnableNetworkAsync(); } + /// public Task ClearPersistenceAsync() { return _firestore.ClearPersistenceAsync(); } + /// public Task TerminateAsync() { return _firestore.TerminateAsync(); } + /// public void Restart() { _firestore = FBFirestore.SharedInstance; } + /// public void UseEmulator(string host, int port) { _firestore.UseEmulatorWithHost(host, (uint) port); Settings = new FirestoreSettings(Settings.Host); } + /// public FirestoreSettings Settings { get => _firestore.Settings.ToAbstract(); set => _firestore.Settings = value.ToNative(); diff --git a/src/Firestore/Platforms/iOS/QuerySnapshotWrapper.cs b/src/Firestore/Platforms/iOS/QuerySnapshotWrapper.cs index 32f4d0d4..75e698ba 100644 --- a/src/Firestore/Platforms/iOS/QuerySnapshotWrapper.cs +++ b/src/Firestore/Platforms/iOS/QuerySnapshotWrapper.cs @@ -3,26 +3,46 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore query snapshot with typed document data. +/// +/// The type to deserialize document data into. public sealed class QuerySnapshotWrapper : IQuerySnapshot { private readonly QuerySnapshot _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS query snapshot to wrap. public QuerySnapshotWrapper(QuerySnapshot querySnapshot) { _wrapped = querySnapshot; } + /// public IEnumerable> GetDocumentChanges(bool includeMetadataChanges) { - return _wrapped - .GetDocumentChanges(includeMetadataChanges) - .Select(x => x.ToAbstract()); + return _wrapped.GetDocumentChanges(includeMetadataChanges).Select(x => x.ToAbstract()); } - public IEnumerable> Documents => _wrapped.Documents.Select(x => x.ToAbstract()); + /// + public IEnumerable> Documents => + _wrapped.Documents.Select(x => x.ToAbstract()); + + /// public ISnapshotMetadata Metadata => _wrapped.Metadata.ToAbstract(); - public IEnumerable> DocumentChanges => _wrapped.DocumentChanges.Select(x => x.ToAbstract()); + + /// + public IEnumerable> DocumentChanges => + _wrapped.DocumentChanges.Select(x => x.ToAbstract()); + + /// public IQuery Query => _wrapped.Query.ToAbstract(); + + /// public bool IsEmpty => _wrapped.IsEmpty; + + /// public int Count => (int) _wrapped.Count; } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/QueryWrapper.cs b/src/Firestore/Platforms/iOS/QueryWrapper.cs index 4207d51b..7caa00c7 100644 --- a/src/Firestore/Platforms/iOS/QueryWrapper.cs +++ b/src/Firestore/Platforms/iOS/QueryWrapper.cs @@ -5,177 +5,238 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore query. +/// public class QueryWrapper : IQuery { private readonly Query _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS query to wrap. public QueryWrapper(Query query) { _wrapped = query; } + /// public IQuery WhereEqualsTo(string field, object value) { return _wrapped.WhereEqualsTo(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereEqualsTo(FieldPath path, object value) { return _wrapped.WhereEqualsTo(path.ToNative(), value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereGreaterThan(string field, object value) { return _wrapped.WhereGreaterThan(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereGreaterThan(FieldPath path, object value) { return _wrapped.WhereGreaterThan(path.ToNative(), value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereLessThan(string field, object value) { return _wrapped.WhereLessThan(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereLessThan(FieldPath path, object value) { return _wrapped.WhereLessThan(path.ToNative(), value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereGreaterThanOrEqualsTo(string field, object value) { return _wrapped.WhereGreaterThanOrEqualsTo(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereGreaterThanOrEqualsTo(FieldPath path, object value) { - return _wrapped.WhereGreaterThanOrEqualsTo(path.ToNative(), value.ToNSObject()).ToAbstract(); + return _wrapped + .WhereGreaterThanOrEqualsTo(path.ToNative(), value.ToNSObject()) + .ToAbstract(); } + /// public IQuery WhereLessThanOrEqualsTo(string field, object value) { return _wrapped.WhereLessThanOrEqualsTo(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereLessThanOrEqualsTo(FieldPath path, object value) { return _wrapped.WhereLessThanOrEqualsTo(path.ToNative(), value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereArrayContains(string field, object value) { return _wrapped.WhereArrayContains(field, value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereArrayContains(FieldPath path, object value) { return _wrapped.WhereArrayContains(path.ToNative(), value.ToNSObject()).ToAbstract(); } + /// public IQuery WhereArrayContainsAny(string field, object[] values) { - return _wrapped.WhereArrayContainsAny(field, values.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .WhereArrayContainsAny(field, values.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery WhereArrayContainsAny(FieldPath path, object[] values) { - return _wrapped.WhereArrayContainsAny(path.ToNative(), values.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .WhereArrayContainsAny(path.ToNative(), values.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery WhereFieldIn(string field, object[] values) { - return _wrapped.WhereFieldIn(field, values.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .WhereFieldIn(field, values.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery WhereFieldIn(FieldPath path, object[] values) { - return _wrapped.WhereFieldIn(path.ToNative(), values.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .WhereFieldIn(path.ToNative(), values.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery OrderBy(string field, bool descending = false) { return _wrapped.OrderedBy(field, descending).ToAbstract(); } + /// public IQuery OrderBy(FieldPath path, bool @descending = false) { return _wrapped.OrderedBy(path.ToNative(), descending).ToAbstract(); } + /// public IQuery StartingAt(params object[] fieldValues) { return _wrapped.StartingAt(fieldValues.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); } + /// public IQuery StartingAt(IDocumentSnapshot snapshot) { return _wrapped.StartingAt(snapshot.ToNative()).ToAbstract(); } + /// public IQuery StartingAfter(params object[] fieldValues) { - return _wrapped.StartingAfter(fieldValues.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .StartingAfter(fieldValues.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery StartingAfter(IDocumentSnapshot snapshot) { return _wrapped.StartingAfter(snapshot.ToNative()).ToAbstract(); } + /// public IQuery EndingAt(params object[] fieldValues) { return _wrapped.EndingAt(fieldValues.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); } + /// public IQuery EndingAt(IDocumentSnapshot snapshot) { return _wrapped.EndingAt(snapshot.ToNative()).ToAbstract(); } + /// public IQuery EndingBefore(params object[] fieldValues) { - return _wrapped.EndingBefore(fieldValues.Select(x => x.ToNSObject()).ToArray()).ToAbstract(); + return _wrapped + .EndingBefore(fieldValues.Select(x => x.ToNSObject()).ToArray()) + .ToAbstract(); } + /// public IQuery EndingBefore(IDocumentSnapshot snapshot) { return _wrapped.EndingBefore(snapshot.ToNative()).ToAbstract(); } + /// public IQuery LimitedTo(int limit) { return _wrapped.LimitedTo(limit).ToAbstract(); } + /// public IQuery LimitedToLast(int limit) { return _wrapped.LimitedToLast(limit).ToAbstract(); } + /// public Task> GetDocumentsAsync(Source source = Source.Default) { var tcs = new TaskCompletionSource>(); - _wrapped.GetDocuments(source.ToNative(), (snapshot, error) => { - if(error == null) { - tcs.SetResult(new QuerySnapshotWrapper(snapshot)); - } else { - tcs.SetException(new FirebaseException(error.LocalizedDescription)); + _wrapped.GetDocuments( + source.ToNative(), + (snapshot, error) => { + if(error == null) { + tcs.SetResult(new QuerySnapshotWrapper(snapshot)); + } else { + tcs.SetException(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return tcs.Task; } - public IDisposable AddSnapshotListener(Action> onChanged, Action onError = null, bool includeMetaDataChanges = false) - { - var registration = _wrapped.AddSnapshotListener(includeMetaDataChanges, (snapshot, error) => { - if(error == null) { - onChanged(new QuerySnapshotWrapper(snapshot)); - } else { - onError?.Invoke(new FirebaseException(error.LocalizedDescription)); + /// + public IDisposable AddSnapshotListener( + Action> onChanged, + Action onError = null, + bool includeMetaDataChanges = false + ) + { + var registration = _wrapped.AddSnapshotListener( + includeMetaDataChanges, + (snapshot, error) => { + if(error == null) { + onChanged(new QuerySnapshotWrapper(snapshot)); + } else { + onError?.Invoke(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return new DisposableWithAction(registration.Remove); } } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/SnapshotMetadataWrapper.cs b/src/Firestore/Platforms/iOS/SnapshotMetadataWrapper.cs index db50d2c2..30520b35 100644 --- a/src/Firestore/Platforms/iOS/SnapshotMetadataWrapper.cs +++ b/src/Firestore/Platforms/iOS/SnapshotMetadataWrapper.cs @@ -2,14 +2,24 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps native iOS Firestore snapshot metadata. +/// public sealed class SnapshotMetadataWrapper : ISnapshotMetadata { + /// + /// Initializes a new instance of the class. + /// + /// The native iOS snapshot metadata to wrap. public SnapshotMetadataWrapper(SnapshotMetadata metadata) { HasPendingWrites = metadata.HasPendingWrites; IsFromCache = metadata.IsFromCache; } + /// public bool HasPendingWrites { get; } + + /// public bool IsFromCache { get; } } \ No newline at end of file diff --git a/src/Firestore/Platforms/iOS/TransactionWrapper.cs b/src/Firestore/Platforms/iOS/TransactionWrapper.cs index 97709777..06c4c2b6 100644 --- a/src/Firestore/Platforms/iOS/TransactionWrapper.cs +++ b/src/Firestore/Platforms/iOS/TransactionWrapper.cs @@ -5,20 +5,29 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore transaction. +/// public sealed class TransactionWrapper : ITransaction { private readonly Transaction _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS transaction to wrap. public TransactionWrapper(Transaction wrapped) { _wrapped = wrapped; } + /// public override string ToString() { return _wrapped.ToString(); } + /// public IDocumentSnapshot GetDocument(IDocumentReference document) { var snapshot = _wrapped.GetDocument(document.ToNative(), out var error).ToAbstract(); @@ -29,12 +38,18 @@ public IDocumentSnapshot GetDocument(IDocumentReference document) } } + /// public ITransaction SetData(IDocumentReference document, object data, SetOptions options = null) { return SetData(document, data.ToDictionary(), options); } - public ITransaction SetData(IDocumentReference document, Dictionary data, SetOptions options = null) + /// + public ITransaction SetData( + IDocumentReference document, + Dictionary data, + SetOptions options = null + ) { if(options == null) { return _wrapped.SetData(data, document.ToNative()).ToAbstract(); @@ -44,34 +59,51 @@ public ITransaction SetData(IDocumentReference document, Dictionary new NativeFieldPath(x.ToArray())).ToArray()).ToAbstract(); + return _wrapped + .SetData( + data, + document.ToNative(), + options.FieldPaths.Select(x => new NativeFieldPath(x.ToArray())).ToArray() + ) + .ToAbstract(); case SetOptions.TypeMergeFields: - return _wrapped.SetData(data, document.ToNative(), options.Fields.ToArray()).ToAbstract(); + return _wrapped + .SetData(data, document.ToNative(), options.Fields.ToArray()) + .ToAbstract(); default: throw new ArgumentException($"SetOptions type {options.Type} is not supported."); } } + /// public ITransaction SetData(IDocumentReference document, params (object, object)[] data) { return SetData(document, data.ToDictionary()); } - public ITransaction SetData(IDocumentReference document, SetOptions options, params (object, object)[] data) + /// + public ITransaction SetData( + IDocumentReference document, + SetOptions options, + params (object, object)[] data + ) { return SetData(document, data.ToDictionary(), options); } + /// public ITransaction UpdateData(IDocumentReference document, Dictionary data) { return _wrapped.UpdateData(data, document.ToNative()).ToAbstract(); } + /// public ITransaction UpdateData(IDocumentReference document, params (string, object)[] data) { return _wrapped.UpdateData(data.ToNSObjectDictionary(), document.ToNative()).ToAbstract(); } + /// public ITransaction DeleteDocument(IDocumentReference document) { return _wrapped.DeleteDocument(document.ToNative()).ToAbstract(); diff --git a/src/Firestore/Platforms/iOS/WriteBatchWrapper.cs b/src/Firestore/Platforms/iOS/WriteBatchWrapper.cs index d5836c95..0f9c5b68 100644 --- a/src/Firestore/Platforms/iOS/WriteBatchWrapper.cs +++ b/src/Firestore/Platforms/iOS/WriteBatchWrapper.cs @@ -4,26 +4,40 @@ namespace Plugin.Firebase.Firestore.Platforms.iOS; +/// +/// Wraps a native iOS Firestore write batch. +/// public sealed class WriteBatchWrapper : IWriteBatch { private readonly WriteBatch _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS write batch to wrap. public WriteBatchWrapper(WriteBatch writeBatch) { _wrapped = writeBatch; } + /// public override string ToString() { return _wrapped.ToString(); } + /// public IWriteBatch SetData(IDocumentReference document, object data, SetOptions options = null) { return SetData(document, data.ToDictionary(), options); } - public IWriteBatch SetData(IDocumentReference document, Dictionary data, SetOptions options = null) + /// + public IWriteBatch SetData( + IDocumentReference document, + Dictionary data, + SetOptions options = null + ) { if(options == null) { return _wrapped.SetData(data, document.ToNative()).ToAbstract(); @@ -33,44 +47,63 @@ public IWriteBatch SetData(IDocumentReference document, Dictionary new NativeFieldPath(x.ToArray())).ToArray()).ToAbstract(); + return _wrapped + .SetData( + data, + document.ToNative(), + options.FieldPaths.Select(x => new NativeFieldPath(x.ToArray())).ToArray() + ) + .ToAbstract(); case SetOptions.TypeMergeFields: - return _wrapped.SetData(data, document.ToNative(), options.Fields.ToArray()).ToAbstract(); + return _wrapped + .SetData(data, document.ToNative(), options.Fields.ToArray()) + .ToAbstract(); default: throw new ArgumentException($"SetOptions type {options.Type} is not supported."); } } + /// public IWriteBatch SetData(IDocumentReference document, params (object, object)[] data) { return SetData(document, data.ToDictionary()); } - public IWriteBatch SetData(IDocumentReference document, SetOptions options, params (object, object)[] data) + /// + public IWriteBatch SetData( + IDocumentReference document, + SetOptions options, + params (object, object)[] data + ) { return SetData(document, data.ToDictionary()); } + /// public IWriteBatch UpdateData(IDocumentReference document, Dictionary data) { return _wrapped.UpdateData(data.ToNSObjectDictionary(), document.ToNative()).ToAbstract(); } + /// public IWriteBatch UpdateData(IDocumentReference document, params (string, object)[] data) { return _wrapped.UpdateData(data.ToNSObjectDictionary(), document.ToNative()).ToAbstract(); } + /// public IWriteBatch DeleteDocument(IDocumentReference document) { return _wrapped.DeleteDocument(document.ToNative()).ToAbstract(); } + /// public Task CommitAsync() { return _wrapped.CommitAsync(); } + /// public void CommitLocal() { _wrapped.Commit(); diff --git a/src/Firestore/Shared/CrossFirebaseFirestore.cs b/src/Firestore/Shared/CrossFirebaseFirestore.cs index 2b9a4656..60abc74b 100644 --- a/src/Firestore/Shared/CrossFirebaseFirestore.cs +++ b/src/Firestore/Shared/CrossFirebaseFirestore.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.Firestore; +/// +/// Cross-platform entry point for Firebase Firestore. +/// public sealed class CrossFirebaseFirestore { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseFirestore CreateInstance() { @@ -34,16 +40,21 @@ public static IFirebaseFirestore Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/Firestore/Shared/DocumentChange.cs b/src/Firestore/Shared/DocumentChange.cs index c2f47d4e..e5dd5dcd 100644 --- a/src/Firestore/Shared/DocumentChange.cs +++ b/src/Firestore/Shared/DocumentChange.cs @@ -1,12 +1,24 @@ namespace Plugin.Firebase.Firestore; +/// +/// Represents a change to a document in a query result set. +/// +/// The type of the document data. public sealed class DocumentChange { + /// + /// Creates a new DocumentChange instance. + /// + /// The snapshot of the changed document. + /// The type of change that occurred. + /// The new index of the document in the result set. + /// The old index of the document in the result set. public DocumentChange( IDocumentSnapshot documentSnapshot, DocumentChangeType changeType, int newIndex, - int oldIndex) + int oldIndex + ) { DocumentSnapshot = documentSnapshot; ChangeType = changeType; @@ -14,8 +26,23 @@ public DocumentChange( OldIndex = oldIndex; } + /// + /// Gets the snapshot of the changed document. + /// public IDocumentSnapshot DocumentSnapshot { get; } + + /// + /// Gets the type of change that occurred (Added, Modified, or Removed). + /// public DocumentChangeType ChangeType { get; } + + /// + /// Gets the new index of the document in the result set after this change, or a special sentinel value if the document was removed. + /// public int NewIndex { get; } + + /// + /// Gets the old index of the document in the result set before this change, or a special sentinel value if the document was added. + /// public int OldIndex { get; } } \ No newline at end of file diff --git a/src/Firestore/Shared/DocumentChangeType.cs b/src/Firestore/Shared/DocumentChangeType.cs index db3e2b9c..e7020ecf 100644 --- a/src/Firestore/Shared/DocumentChangeType.cs +++ b/src/Firestore/Shared/DocumentChangeType.cs @@ -1,8 +1,22 @@ namespace Plugin.Firebase.Firestore; +/// +/// Specifies the type of change to a document. +/// public enum DocumentChangeType { + /// + /// Indicates a new document was added to the result set. + /// Added, + + /// + /// Indicates an existing document was modified. + /// Modified, - Removed + + /// + /// Indicates a document was removed from the result set. + /// + Removed, } \ No newline at end of file diff --git a/src/Firestore/Shared/FieldPath.cs b/src/Firestore/Shared/FieldPath.cs index 97439eee..1713b836 100644 --- a/src/Firestore/Shared/FieldPath.cs +++ b/src/Firestore/Shared/FieldPath.cs @@ -1,5 +1,8 @@ namespace Plugin.Firebase.Firestore; +/// +/// Represents a path to a field in a Firestore document. +/// public sealed class FieldPath { private FieldPath(string[] fields = null, bool isDocumentId = false) @@ -8,16 +11,32 @@ private FieldPath(string[] fields = null, bool isDocumentId = false) IsDocumentId = isDocumentId; } + /// + /// Creates a FieldPath from the provided field names. + /// + /// An array of field names representing the path. + /// A new FieldPath instance. public static FieldPath Of(string[] fields) { return new FieldPath(fields); } + /// + /// Returns a special sentinel FieldPath to refer to the document ID. + /// + /// A FieldPath representing the document ID. public static FieldPath DocumentId() { return new FieldPath(isDocumentId: true); } + /// + /// Gets the array of field names that make up this path. + /// public string[] Fields { get; } + + /// + /// Gets a value indicating whether this path refers to the document ID. + /// public bool IsDocumentId { get; } } \ No newline at end of file diff --git a/src/Firestore/Shared/FieldValue.cs b/src/Firestore/Shared/FieldValue.cs index a2e06034..2b9c35ef 100644 --- a/src/Firestore/Shared/FieldValue.cs +++ b/src/Firestore/Shared/FieldValue.cs @@ -3,7 +3,7 @@ namespace Plugin.Firebase.Firestore; /// /// Sentinel values that can be used when writing document fields with Set() or Update(). /// -/// +/// public sealed class FieldValue { /// @@ -44,15 +44,13 @@ public static FieldValue DoubleIncrement(double incrementValue) => /// /// Returns a sentinel for use with Update() to mark a field for deletion. /// - public static FieldValue Delete() => - new FieldValue(FieldValueType.Delete); + public static FieldValue Delete() => new FieldValue(FieldValueType.Delete); /// /// Returns a sentinel for use with Set() or Update() to include a server-generated timestamp in the written data. /// /// - public static FieldValue ServerTimestamp() => - new FieldValue(FieldValueType.ServerTimestamp); + public static FieldValue ServerTimestamp() => new FieldValue(FieldValueType.ServerTimestamp); private FieldValue(FieldValueType type, double incrementValue = 0, object[] elements = null) { @@ -61,7 +59,18 @@ private FieldValue(FieldValueType type, double incrementValue = 0, object[] elem IncrementValue = incrementValue; } + /// + /// Gets the type of this FieldValue operation. + /// public FieldValueType Type { get; } + + /// + /// Gets the elements for array operations (ArrayUnion, ArrayRemove). + /// public object[] Elements { get; } + + /// + /// Gets the increment value for numeric increment operations. + /// public double IncrementValue { get; } } \ No newline at end of file diff --git a/src/Firestore/Shared/FieldValueType.cs b/src/Firestore/Shared/FieldValueType.cs index 17bff220..3e4b78df 100644 --- a/src/Firestore/Shared/FieldValueType.cs +++ b/src/Firestore/Shared/FieldValueType.cs @@ -1,6 +1,37 @@ namespace Plugin.Firebase.Firestore; +/// +/// Specifies the type of a sentinel operation. +/// public enum FieldValueType { - ArrayUnion, ArrayRemove, IntegerIncrement, DoubleIncrement, Delete, ServerTimestamp + /// + /// Union elements into an array field. + /// + ArrayUnion, + + /// + /// Remove elements from an array field. + /// + ArrayRemove, + + /// + /// Increment a numeric field by an integer value. + /// + IntegerIncrement, + + /// + /// Increment a numeric field by a double value. + /// + DoubleIncrement, + + /// + /// Delete a field from a document. + /// + Delete, + + /// + /// Set a field to the server-generated timestamp. + /// + ServerTimestamp, } \ No newline at end of file diff --git a/src/Firestore/Shared/FirestoreExtensions.cs b/src/Firestore/Shared/FirestoreExtensions.cs index d8324928..17ac9c4d 100644 --- a/src/Firestore/Shared/FirestoreExtensions.cs +++ b/src/Firestore/Shared/FirestoreExtensions.cs @@ -1,10 +1,28 @@ namespace Plugin.Firebase.Firestore; +/// +/// Extension methods for Firestore operations. +/// public static class FirestoreExtensions { - public static async Task DeleteCollectionAsync(this IFirebaseFirestore @this, string collectionPath, int batchSize) + /// + /// Deletes all documents in a collection in batches. + /// + /// The type of the document data. + /// The Firestore instance. + /// The path to the collection to delete. + /// The maximum number of documents to delete per batch. + /// A task that completes when all documents have been deleted. + public static async Task DeleteCollectionAsync( + this IFirebaseFirestore @this, + string collectionPath, + int batchSize + ) { - var snapshot = await @this.GetCollection(collectionPath).LimitedTo(batchSize).GetDocumentsAsync(); + var snapshot = await @this + .GetCollection(collectionPath) + .LimitedTo(batchSize) + .GetDocumentsAsync(); if(snapshot.Documents.Any()) { var batch = @this.CreateBatch(); diff --git a/src/Firestore/Shared/FirestorePropertyAttribute.cs b/src/Firestore/Shared/FirestorePropertyAttribute.cs index ce13e019..b82f2b95 100644 --- a/src/Firestore/Shared/FirestorePropertyAttribute.cs +++ b/src/Firestore/Shared/FirestorePropertyAttribute.cs @@ -1,26 +1,49 @@ namespace Plugin.Firebase.Firestore; +/// +/// Specifies a custom property name to use when serializing or deserializing a property to/from Firestore. +/// [AttributeUsage(AttributeTargets.Property)] public class FirestorePropertyAttribute : Attribute { + /// + /// Creates a new FirestorePropertyAttribute with the specified property name. + /// + /// The name to use in Firestore for this property. public FirestorePropertyAttribute(string propertyName) { PropertyName = propertyName; } + /// + /// Gets the property name to use in Firestore. + /// public string PropertyName { get; } } +/// +/// Indicates that a property should be populated with the document ID when reading from Firestore. +/// [AttributeUsage(AttributeTargets.Property)] public class FirestoreDocumentIdAttribute : Attribute; +/// +/// Indicates that a property should be set to the server timestamp when writing to Firestore. +/// [AttributeUsage(AttributeTargets.Property)] public class FirestoreServerTimestampAttribute : Attribute { + /// + /// Creates a new FirestoreServerTimestampAttribute with the specified property name. + /// + /// The name to use in Firestore for this property. public FirestoreServerTimestampAttribute(string propertyName) { PropertyName = propertyName; } + /// + /// Gets the property name to use in Firestore. + /// public string PropertyName { get; } } \ No newline at end of file diff --git a/src/Firestore/Shared/FirestoreSettings.cs b/src/Firestore/Shared/FirestoreSettings.cs index 590771e8..9bfba23b 100644 --- a/src/Firestore/Shared/FirestoreSettings.cs +++ b/src/Firestore/Shared/FirestoreSettings.cs @@ -5,9 +5,12 @@ namespace Plugin.Firebase.Firestore; /// public sealed class FirestoreSettings { - public FirestoreSettings( - string host = null, - bool isSslEnabled = false) + /// + /// Creates a new FirestoreSettings instance. + /// + /// The host of the Cloud Firestore backend. + /// Whether to use SSL for communication. + public FirestoreSettings(string host = null, bool isSslEnabled = false) { Host = host; IsSslEnabled = isSslEnabled; diff --git a/src/Firestore/Shared/GeoPoint.cs b/src/Firestore/Shared/GeoPoint.cs index 392473f9..e77b120b 100644 --- a/src/Firestore/Shared/GeoPoint.cs +++ b/src/Firestore/Shared/GeoPoint.cs @@ -1,16 +1,32 @@ namespace Plugin.Firebase.Firestore; +/// +/// Represents a geographic location by its latitude and longitude. +/// public class GeoPoint { + /// + /// Creates a new GeoPoint with the specified latitude and longitude. + /// + /// The latitude value in degrees. + /// The longitude value in degrees. public GeoPoint(double latitude, double longitude) { Latitude = latitude; Longitude = longitude; } + /// + /// Gets the latitude value in degrees. + /// public double Latitude { get; } + + /// + /// Gets the longitude value in degrees. + /// public double Longitude { get; } + /// public override string ToString() { return $"[{nameof(Latitude)}={Latitude}, {nameof(Longitude)}={Longitude}]"; diff --git a/src/Firestore/Shared/IDocumentSnapshot.cs b/src/Firestore/Shared/IDocumentSnapshot.cs index c07544ce..00ff9111 100644 --- a/src/Firestore/Shared/IDocumentSnapshot.cs +++ b/src/Firestore/Shared/IDocumentSnapshot.cs @@ -24,6 +24,7 @@ public interface IDocumentSnapshot : IDocumentSnapshot IDocumentReference Reference { get; } } -public interface IDocumentSnapshot -{ -} \ No newline at end of file +/// +/// Base interface for document snapshots. +/// +public interface IDocumentSnapshot { } \ No newline at end of file diff --git a/src/Firestore/Shared/IFirestoreObject.cs b/src/Firestore/Shared/IFirestoreObject.cs index 6193a3f3..15098bfa 100644 --- a/src/Firestore/Shared/IFirestoreObject.cs +++ b/src/Firestore/Shared/IFirestoreObject.cs @@ -1,5 +1,6 @@ namespace Plugin.Firebase.Firestore; -public interface IFirestoreObject -{ -} \ No newline at end of file +/// +/// Marker interface for objects that can be serialized to and from Firestore documents. +/// +public interface IFirestoreObject { } \ No newline at end of file diff --git a/src/Firestore/Shared/SetOptions.cs b/src/Firestore/Shared/SetOptions.cs index a4fb2a3a..43a34fdd 100644 --- a/src/Firestore/Shared/SetOptions.cs +++ b/src/Firestore/Shared/SetOptions.cs @@ -20,7 +20,8 @@ public sealed class SetOptions /// the fields specified here in its to data argument. /// /// The list of fields to merge. - public static SetOptions MergeFieldPaths(IList> fieldPaths) => new SetOptions(TypeMergeFieldPaths, fieldPaths); + public static SetOptions MergeFieldPaths(IList> fieldPaths) => + new SetOptions(TypeMergeFieldPaths, fieldPaths); /// /// Changes the behavior of Set() calls to only replace the given fields. Any field that is not specified in fields is ignored @@ -28,7 +29,8 @@ public sealed class SetOptions /// fields specified here. /// /// The list of fields to merge. Fields can contain dots to reference nested fields within the document. - public static SetOptions MergeFields(params string[] fields) => new SetOptions(TypeMergeFields, fields: fields); + public static SetOptions MergeFields(params string[] fields) => + new SetOptions(TypeMergeFields, fields: fields); /// /// Changes the behavior of Set() calls to only replace the given fields. Any field that is not specified in fields is ignored @@ -36,12 +38,30 @@ public sealed class SetOptions /// fields specified here. /// /// The list of fields to merge. Fields can contain dots to reference nested fields within the document. - public static SetOptions MergeFields(IList fields) => new SetOptions(TypeMergeFields, fields: fields); + public static SetOptions MergeFields(IList fields) => + new SetOptions(TypeMergeFields, fields: fields); + /// + /// Constant representing a merge-all operation. + /// public const int TypeMerge = 0; + + /// + /// Constant representing a merge with field paths operation. + /// public const int TypeMergeFieldPaths = 1; + + /// + /// Constant representing a merge with field names operation. + /// public const int TypeMergeFields = 2; + /// + /// Creates a new SetOptions instance. + /// + /// The type of merge operation. + /// The field paths to merge. + /// The field names to merge. public SetOptions(int type, IList> fieldPaths = null, IList fields = null) { Type = type; diff --git a/src/Firestore/Shared/Source.cs b/src/Firestore/Shared/Source.cs index 47c138af..6a67948e 100644 --- a/src/Firestore/Shared/Source.cs +++ b/src/Firestore/Shared/Source.cs @@ -7,5 +7,18 @@ namespace Plugin.Firebase.Firestore; /// public enum Source { - Default, Cache, Server + /// + /// Attempt to fetch from the server first, falling back to the local cache if unavailable. + /// + Default, + + /// + /// Fetch results only from the local cache. + /// + Cache, + + /// + /// Fetch results only from the server. + /// + Server, } \ No newline at end of file diff --git a/src/Functions/Functions.csproj b/src/Functions/Functions.csproj index 8aceca85..d42c97fb 100644 --- a/src/Functions/Functions.csproj +++ b/src/Functions/Functions.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.Functions - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/Functions/Platforms/iOS/FirebaseFunctionsImplementation.cs b/src/Functions/Platforms/iOS/FirebaseFunctionsImplementation.cs index 222b1f3c..067c96b4 100644 --- a/src/Functions/Platforms/iOS/FirebaseFunctionsImplementation.cs +++ b/src/Functions/Platforms/iOS/FirebaseFunctionsImplementation.cs @@ -4,25 +4,37 @@ namespace Plugin.Firebase.Functions; +/// +/// iOS implementation of that wraps the native Firebase Cloud Functions SDK. +/// public sealed class FirebaseFunctionsImplementation : DisposableBase, IFirebaseFunctions { private readonly CloudFunctions _functions; + /// + /// Initializes a new instance of the class using the default region. + /// public FirebaseFunctionsImplementation() { _functions = CloudFunctions.DefaultInstance; } + /// + /// Initializes a new instance of the class for a specific region. + /// + /// The region where the Cloud Functions are deployed. public FirebaseFunctionsImplementation(string region) { _functions = CloudFunctions.FromRegion(region); } + /// public IHttpsCallable GetHttpsCallable(string name) { return new HttpsCallableWrapper(_functions.HttpsCallable(name)); } + /// public void UseEmulator(string host, int port) { _functions.UseEmulatorOriginWithHost(host, (uint) port); diff --git a/src/Functions/Platforms/iOS/HttpsCallableWrapper.cs b/src/Functions/Platforms/iOS/HttpsCallableWrapper.cs index 84ba0799..49bb597f 100644 --- a/src/Functions/Platforms/iOS/HttpsCallableWrapper.cs +++ b/src/Functions/Platforms/iOS/HttpsCallableWrapper.cs @@ -4,29 +4,44 @@ namespace Plugin.Firebase.Functions.Platforms.iOS; +/// +/// iOS implementation of that wraps the native type. +/// public sealed class HttpsCallableWrapper : IHttpsCallable { private readonly HttpsCallable _httpsCallable; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS HTTPS callable to wrap. public HttpsCallableWrapper(HttpsCallable httpsCallable) { _httpsCallable = httpsCallable; } + /// public Task CallAsync(string dataJson = null) { - return dataJson == null ? _httpsCallable.CallAsync() : _httpsCallable.CallAsync(ConvertJsonToData(dataJson)); + return dataJson == null + ? _httpsCallable.CallAsync() + : _httpsCallable.CallAsync(ConvertJsonToData(dataJson)); } private static NSObject ConvertJsonToData(string dataJson) { - var data = NSJsonSerialization.Deserialize(NSData.FromString(dataJson, NSStringEncoding.UTF8), 0, out var error); + var data = NSJsonSerialization.Deserialize( + NSData.FromString(dataJson, NSStringEncoding.UTF8), + 0, + out var error + ); if(error != null) { throw new FirebaseException(error.LocalizedDescription); } return data; } + /// public async Task CallAsync(string dataJson = null) { var result = await _httpsCallable.CallAsync(ConvertJsonToData(dataJson)); diff --git a/src/Functions/Shared/CrossFirebaseFunctions.cs b/src/Functions/Shared/CrossFirebaseFunctions.cs index 776f2a69..a31a309d 100644 --- a/src/Functions/Shared/CrossFirebaseFunctions.cs +++ b/src/Functions/Shared/CrossFirebaseFunctions.cs @@ -1,14 +1,22 @@ namespace Plugin.Firebase.Functions; +/// +/// Cross-platform entry point for Firebase Functions. +/// public sealed class CrossFirebaseFunctions { private static string _region; - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseFunctions CreateInstance() { #if IOS || ANDROID - return _region == null ? new FirebaseFunctionsImplementation() : new FirebaseFunctionsImplementation(_region); + return _region == null + ? new FirebaseFunctionsImplementation() + : new FirebaseFunctionsImplementation(_region); #else #pragma warning disable IDE0022 // Use expression body for methods return null; @@ -44,16 +52,21 @@ public static IFirebaseFunctions Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/RemoteConfig/Platforms/iOS/Extensions/DictionaryExtensions.cs b/src/RemoteConfig/Platforms/iOS/Extensions/DictionaryExtensions.cs index ad10ed71..b8c1c4c3 100644 --- a/src/RemoteConfig/Platforms/iOS/Extensions/DictionaryExtensions.cs +++ b/src/RemoteConfig/Platforms/iOS/Extensions/DictionaryExtensions.cs @@ -2,31 +2,55 @@ namespace Plugin.Firebase.RemoteConfig.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting dictionaries to native iOS NSDictionary types. +/// public static class DictionaryExtensions { - public static NSDictionary ToNSDictionary(this IDictionary dictionary) + /// + /// Converts a generic dictionary to an NSDictionary suitable for Firebase Remote Config. + /// + /// The dictionary to convert. + /// An NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionary( + this IDictionary dictionary + ) { return ((IDictionary) dictionary).ToNSDictionaryFromNonGeneric(); } - public static NSDictionary ToNSDictionaryFromNonGeneric(this IDictionary dictionary) + /// + /// Converts a non-generic dictionary to an NSDictionary suitable for Firebase Remote Config. + /// + /// The non-generic dictionary to convert. + /// An NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionaryFromNonGeneric( + this IDictionary dictionary + ) { if(dictionary.Count > 0) { var nsDictionary = new NSMutableDictionary(); foreach(DictionaryEntry entry in dictionary) { - PutIntoNSDictionary(new KeyValuePair(entry.Key.ToString(), entry.Value), ref nsDictionary); + PutIntoNSDictionary( + new KeyValuePair(entry.Key.ToString(), entry.Value), + ref nsDictionary + ); } return NSDictionary.FromObjectsAndKeys( nsDictionary.Values.ToArray(), nsDictionary.Keys.ToArray(), - (nint) nsDictionary.Count); + (nint) nsDictionary.Count + ); } else { return new NSDictionary(); } } - private static void PutIntoNSDictionary(KeyValuePair pair, ref NSMutableDictionary nsDictionary) + private static void PutIntoNSDictionary( + KeyValuePair pair, + ref NSMutableDictionary nsDictionary + ) { switch(pair.Value) { case bool x: @@ -61,12 +85,21 @@ private static void PutIntoNSDictionary(KeyValuePair pair, ref N nsDictionary.Add((NSString) pair.Key, new NSNull()); break; } else { - throw new ArgumentException($"Couldn't put object of type {pair.Value.GetType()} into NSDictionary"); + throw new ArgumentException( + $"Couldn't put object of type {pair.Value.GetType()} into NSDictionary" + ); } } } - public static NSDictionary ToNSDictionary(this IEnumerable<(string, object)> tuples) + /// + /// Converts an enumerable of tuples to an NSDictionary suitable for Firebase Remote Config. + /// + /// The tuples to convert, where each tuple contains a key and value. + /// An NSDictionary containing the converted key-value pairs. + public static NSDictionary ToNSDictionary( + this IEnumerable<(string, object)> tuples + ) { var dict = new Dictionary(); tuples.ToList().ForEach(x => dict.Add(x.Item1, x.Item2)); diff --git a/src/RemoteConfig/Platforms/iOS/Extensions/RemoteConfigExtensions.cs b/src/RemoteConfig/Platforms/iOS/Extensions/RemoteConfigExtensions.cs index a6d86ddc..dba7d4eb 100644 --- a/src/RemoteConfig/Platforms/iOS/Extensions/RemoteConfigExtensions.cs +++ b/src/RemoteConfig/Platforms/iOS/Extensions/RemoteConfigExtensions.cs @@ -2,20 +2,34 @@ namespace Plugin.Firebase.RemoteConfig.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting between abstract and native iOS Firebase Remote Config settings. +/// public static class RemoteConfigExtensions { + /// + /// Converts abstract RemoteConfigSettings to native iOS Firebase RemoteConfigSettings. + /// + /// The abstract settings to convert. + /// Native iOS Firebase RemoteConfigSettings. public static NativeRemoteConfigSettings ToNative(this RemoteConfigSettings @this) { return new NativeRemoteConfigSettings { MinimumFetchInterval = @this.MinimumFetchInterval.TotalSeconds, - FetchTimeout = @this.FetchTimeout.TotalSeconds + FetchTimeout = @this.FetchTimeout.TotalSeconds, }; } + /// + /// Converts native iOS Firebase RemoteConfigSettings to abstract RemoteConfigSettings. + /// + /// The native settings to convert. + /// Abstract RemoteConfigSettings. public static RemoteConfigSettings ToAbstract(this NativeRemoteConfigSettings @this) { return new RemoteConfigSettings( TimeSpan.FromSeconds(@this.MinimumFetchInterval), - TimeSpan.FromSeconds(@this.FetchTimeout)); + TimeSpan.FromSeconds(@this.FetchTimeout) + ); } } \ No newline at end of file diff --git a/src/RemoteConfig/Platforms/iOS/FirebaseRemoteConfigImplementation.cs b/src/RemoteConfig/Platforms/iOS/FirebaseRemoteConfigImplementation.cs index 2d958bdd..1b5141b1 100644 --- a/src/RemoteConfig/Platforms/iOS/FirebaseRemoteConfigImplementation.cs +++ b/src/RemoteConfig/Platforms/iOS/FirebaseRemoteConfigImplementation.cs @@ -4,66 +4,91 @@ namespace Plugin.Firebase.RemoteConfig; +/// +/// iOS implementation of Firebase Remote Config that wraps the native iOS Firebase Remote Config SDK. +/// public sealed class FirebaseRemoteConfigImplementation : DisposableBase, IFirebaseRemoteConfig { private readonly FirebaseRemoteConfig _remoteConfig; + /// + /// Initializes a new instance of the class. + /// public FirebaseRemoteConfigImplementation() { _remoteConfig = FirebaseRemoteConfig.SharedInstance; } + /// public Task EnsureInitializedAsync() { return _remoteConfig.EnsureInitializedAsync(); } + /// public Task SetRemoteConfigSettingsAsync(RemoteConfigSettings configSettings) { _remoteConfig.ConfigSettings = configSettings.ToNative(); return Task.CompletedTask; } + /// public Task SetDefaultsAsync(IDictionary defaults) { _remoteConfig.SetDefaults(defaults.ToNSDictionary()); return Task.CompletedTask; } + /// public Task SetDefaultsAsync(params (string, object)[] defaults) { _remoteConfig.SetDefaults(defaults.ToNSDictionary()); return Task.CompletedTask; } + /// public Task FetchAndActivateAsync() { return _remoteConfig.FetchAndActivateAsync(); } + /// public Task FetchAsync(double expirationDuration = 3600) { return _remoteConfig.FetchAsync(expirationDuration); } + /// public Task ActivateAsync() { return _remoteConfig.ActivateAsync(); } + /// public IEnumerable GetKeysByPrefix(string prefix) { return _remoteConfig.GetKeys(prefix).ToArray().Select(x => (string) x); } + /// public bool GetBoolean(string key) => _remoteConfig.GetConfigValue(key).BoolValue; + + /// public string GetString(string key) => _remoteConfig.GetConfigValue(key).StringValue; + + /// public long GetLong(string key) => (long) _remoteConfig.GetConfigValue(key).NumberValue; + + /// public double GetDouble(string key) => (double) _remoteConfig.GetConfigValue(key).NumberValue; + /// public RemoteConfigInfo Info => new RemoteConfigInfo( _remoteConfig.ConfigSettings.ToAbstract(), - DateTimeOffset.FromUnixTimeSeconds((long) (_remoteConfig.LastFetchTime?.SecondsSince1970 ?? 0)), - (RemoteConfigFetchStatus) (long) _remoteConfig.LastFetchStatus); + DateTimeOffset.FromUnixTimeSeconds( + (long) (_remoteConfig.LastFetchTime?.SecondsSince1970 ?? 0) + ), + (RemoteConfigFetchStatus) (long) _remoteConfig.LastFetchStatus + ); } \ No newline at end of file diff --git a/src/RemoteConfig/RemoteConfig.csproj b/src/RemoteConfig/RemoteConfig.csproj index 5bf1e683..16d8cfb0 100644 --- a/src/RemoteConfig/RemoteConfig.csproj +++ b/src/RemoteConfig/RemoteConfig.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.RemoteConfig - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/src/RemoteConfig/Shared/RemoteConfig/CrossFirebaseRemoteConfig.cs b/src/RemoteConfig/Shared/RemoteConfig/CrossFirebaseRemoteConfig.cs index cae0bd2c..26c857b6 100644 --- a/src/RemoteConfig/Shared/RemoteConfig/CrossFirebaseRemoteConfig.cs +++ b/src/RemoteConfig/Shared/RemoteConfig/CrossFirebaseRemoteConfig.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.RemoteConfig; +/// +/// Cross-platform entry point for Firebase Remote Config. +/// public sealed class CrossFirebaseRemoteConfig { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseRemoteConfig CreateInstance() { @@ -34,16 +40,21 @@ public static IFirebaseRemoteConfig Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigFetchStatus.cs b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigFetchStatus.cs index f77a7f7c..f78d5d5c 100644 --- a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigFetchStatus.cs +++ b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigFetchStatus.cs @@ -1,9 +1,27 @@ namespace Plugin.Firebase.RemoteConfig; +/// +/// Indicates the status of the most recent Remote Config fetch attempt. +/// public enum RemoteConfigFetchStatus : long { + /// + /// No fetch has been attempted yet. + /// NoFetchYet, + + /// + /// The last fetch was successful. + /// Success, + + /// + /// The last fetch failed. + /// Failure, - Throttled + + /// + /// The last fetch was throttled due to too many requests. + /// + Throttled, } \ No newline at end of file diff --git a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigInfo.cs b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigInfo.cs index ba570914..638d9904 100644 --- a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigInfo.cs +++ b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigInfo.cs @@ -5,7 +5,17 @@ namespace Plugin.Firebase.RemoteConfig; /// public sealed class RemoteConfigInfo { - public RemoteConfigInfo(RemoteConfigSettings configSettings, DateTimeOffset lastFetchTime, RemoteConfigFetchStatus lastFetchStatus) + /// + /// Creates a new RemoteConfigInfo instance. + /// + /// The current Remote Config settings. + /// The timestamp of the last successful fetch. + /// The status of the most recent fetch attempt. + public RemoteConfigInfo( + RemoteConfigSettings configSettings, + DateTimeOffset lastFetchTime, + RemoteConfigFetchStatus lastFetchStatus + ) { ConfigSettings = configSettings; LastFetchTime = lastFetchTime; @@ -13,17 +23,17 @@ public RemoteConfigInfo(RemoteConfigSettings configSettings, DateTimeOffset last } /// - /// Gets the current settings of the FirebaseRemoteConfig singleton object. + /// Gets the current settings of the FirebaseRemoteConfig singleton object. /// public RemoteConfigSettings ConfigSettings { get; } /// - /// Gets the timestamp (milliseconds since epoch) of the last successful fetch, regardless of whether the fetch was activated or not. + /// Gets the timestamp (milliseconds since epoch) of the last successful fetch, regardless of whether the fetch was activated or not. /// public DateTimeOffset LastFetchTime { get; } /// - /// Gets the status of the most recent fetch attempt. + /// Gets the status of the most recent fetch attempt. /// public RemoteConfigFetchStatus LastFetchStatus { get; } } \ No newline at end of file diff --git a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigSettings.cs b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigSettings.cs index 4e02d73f..a52fa521 100644 --- a/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigSettings.cs +++ b/src/RemoteConfig/Shared/RemoteConfig/RemoteConfigSettings.cs @@ -5,28 +5,38 @@ namespace Plugin.Firebase.RemoteConfig; /// public sealed class RemoteConfigSettings { + /// + /// Creates a new RemoteConfigSettings instance. + /// + /// The minimum interval between successive fetches. Defaults to 3600 seconds (1 hour). + /// The timeout for fetch operations. Defaults to 60 seconds. public RemoteConfigSettings( TimeSpan? minimumFetchInterval = null, - TimeSpan? fetchTimeout = null) + TimeSpan? fetchTimeout = null + ) { MinimumFetchInterval = minimumFetchInterval ?? TimeSpan.FromSeconds(3600); FetchTimeout = fetchTimeout ?? TimeSpan.FromSeconds(60); } + /// public override bool Equals(object obj) { if(obj is RemoteConfigSettings other) { - return (MinimumFetchInterval, FetchTimeout).Equals((other.MinimumFetchInterval, other.FetchTimeout)); - + return (MinimumFetchInterval, FetchTimeout).Equals( + (other.MinimumFetchInterval, other.FetchTimeout) + ); } return false; } + /// public override int GetHashCode() { return (MinimumFetchInterval, FetchTimeout).GetHashCode(); } + /// public override string ToString() { return $"[{nameof(RemoteConfigSettings)}: {nameof(MinimumFetchInterval)}={MinimumFetchInterval}, {nameof(FetchTimeout)}={FetchTimeout}]"; @@ -38,7 +48,7 @@ public override string ToString() public TimeSpan MinimumFetchInterval { get; } /// - /// Returns the fetch timeout in seconds. + /// Returns the fetch timeout in seconds. /// public TimeSpan FetchTimeout { get; } } \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/Extensions/DictionaryExtensions.cs b/src/Storage/Platforms/iOS/Extensions/DictionaryExtensions.cs index 515d0fc5..adcc1e5f 100644 --- a/src/Storage/Platforms/iOS/Extensions/DictionaryExtensions.cs +++ b/src/Storage/Platforms/iOS/Extensions/DictionaryExtensions.cs @@ -1,8 +1,18 @@ namespace Plugin.Firebase.Storage.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting between .NET dictionaries and native iOS NSDictionary types for Firebase Storage. +/// public static class DictionaryExtensions { - public static IDictionary ToDictionary(this NSDictionary @this) + /// + /// Converts a native NSDictionary to a .NET dictionary. + /// + /// The NSDictionary to convert. + /// A .NET dictionary containing the key-value pairs. + public static IDictionary ToDictionary( + this NSDictionary @this + ) { var dict = new Dictionary(); foreach(var (key, value) in @this) { @@ -11,9 +21,18 @@ public static IDictionary ToDictionary(this NSDictionary ToNSDictionary(this IDictionary @this) + /// + /// Converts a .NET dictionary to a native NSDictionary. + /// + /// The dictionary to convert. + /// An NSDictionary containing the key-value pairs. + public static NSDictionary ToNSDictionary( + this IDictionary @this + ) { - return NSDictionary.FromObjectsAndKeys(@this.Values.ToArray(), - @this.Keys.ToArray()); + return NSDictionary.FromObjectsAndKeys( + @this.Values.ToArray(), + @this.Keys.ToArray() + ); } } \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/Extensions/StorageExtensions.cs b/src/Storage/Platforms/iOS/Extensions/StorageExtensions.cs index f1ac14ef..cf976c07 100644 --- a/src/Storage/Platforms/iOS/Extensions/StorageExtensions.cs +++ b/src/Storage/Platforms/iOS/Extensions/StorageExtensions.cs @@ -1,27 +1,51 @@ using Firebase.Storage; using Plugin.Firebase.Core.Platforms.iOS.Extensions; -using NativeStorageTaskStatus = Firebase.Storage.StorageTaskStatus; using NativeStorageMetadata = Firebase.Storage.StorageMetadata; +using NativeStorageTaskStatus = Firebase.Storage.StorageTaskStatus; namespace Plugin.Firebase.Storage.Platforms.iOS.Extensions; +/// +/// Provides extension methods for converting between abstract and native iOS Firebase Storage types. +/// public static class StorageExtensions { + /// + /// Converts a native iOS StorageReference to an abstract IStorageReference. + /// + /// The native storage reference to convert. + /// An abstract storage reference wrapper. public static IStorageReference ToAbstract(this StorageReference @this) { return new StorageReferenceWrapper(@this); } + /// + /// Converts a native iOS StorageTaskSnapshot to an abstract IStorageTaskSnapshot. + /// + /// The native snapshot to convert. + /// An abstract storage task snapshot. public static IStorageTaskSnapshot ToAbstract(this StorageTaskSnapshot @this) { return StorageTaskTaskSnapshotWrapper.FromSnapshot(@this); } + /// + /// Converts a native iOS StorageListResult to an abstract IStorageListResult. + /// + /// The native list result to convert. + /// An abstract storage list result wrapper. public static IStorageListResult ToAbstract(this StorageListResult @this) { return new StorageListResultWrapper(@this); } + /// + /// Converts an abstract StorageTaskStatus to a native iOS StorageTaskStatus. + /// + /// The abstract status to convert. + /// The corresponding native iOS storage task status. + /// Thrown when the status cannot be converted. public static NativeStorageTaskStatus ToNative(this StorageTaskStatus @this) { switch(@this) { @@ -36,10 +60,17 @@ public static NativeStorageTaskStatus ToNative(this StorageTaskStatus @this) case StorageTaskStatus.Failure: return NativeStorageTaskStatus.Failure; default: - throw new ArgumentException($"Couldn't convert {nameof(StorageTaskStatus)} {@this} to native status"); + throw new ArgumentException( + $"Couldn't convert {nameof(StorageTaskStatus)} {@this} to native status" + ); } } + /// + /// Converts native iOS StorageMetadata to an abstract IStorageMetadata. + /// + /// The native metadata to convert. + /// An abstract storage metadata object. public static IStorageMetadata ToAbstract(this NativeStorageMetadata @this) { return new StorageMetadata( @@ -57,9 +88,15 @@ public static IStorageMetadata ToAbstract(this NativeStorageMetadata @this) customMetadata: @this.CustomMetadata?.ToDictionary(), md5Hash: @this.Md5Hash, creationTime: @this.TimeCreated.ToDateTimeOffset(), - updatedTime: @this.Updated.ToDateTimeOffset()); + updatedTime: @this.Updated.ToDateTimeOffset() + ); } + /// + /// Converts abstract IStorageMetadata to native iOS StorageMetadata. + /// + /// The abstract metadata to convert. + /// Native iOS storage metadata. public static NativeStorageMetadata ToNative(this IStorageMetadata @this) { return new NativeStorageMetadata { @@ -67,7 +104,7 @@ public static NativeStorageMetadata ToNative(this IStorageMetadata @this) ContentEncoding = @this.ContentEncoding, ContentLanguage = @this.ContentLanguage, ContentType = @this.ContentType, - CustomMetadata = @this.CustomMetadata?.ToNSDictionary() + CustomMetadata = @this.CustomMetadata?.ToNSDictionary(), }; } } \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/FirebaseStorageImplementation.cs b/src/Storage/Platforms/iOS/FirebaseStorageImplementation.cs index 80b398d1..e334b777 100644 --- a/src/Storage/Platforms/iOS/FirebaseStorageImplementation.cs +++ b/src/Storage/Platforms/iOS/FirebaseStorageImplementation.cs @@ -4,25 +4,34 @@ namespace Plugin.Firebase.Storage; +/// +/// iOS implementation of Firebase Storage that wraps the native iOS Firebase Storage SDK. +/// public sealed class FirebaseStorageImplementation : DisposableBase, IFirebaseStorage { private readonly FirebaseStorage _instance; + /// + /// Initializes a new instance of the class. + /// public FirebaseStorageImplementation() { _instance = FirebaseStorage.DefaultInstance; } + /// public IStorageReference GetRootReference() { return _instance.GetRootReference().ToAbstract(); } + /// public IStorageReference GetReferenceFromUrl(string url) { return _instance.GetReferenceFromUrl(url).ToAbstract(); } + /// public IStorageReference GetReferenceFromPath(string path) { return _instance.GetReferenceFromPath(path).ToAbstract(); diff --git a/src/Storage/Platforms/iOS/StorageListResultsWrapper.cs b/src/Storage/Platforms/iOS/StorageListResultsWrapper.cs index 9059b542..df975632 100644 --- a/src/Storage/Platforms/iOS/StorageListResultsWrapper.cs +++ b/src/Storage/Platforms/iOS/StorageListResultsWrapper.cs @@ -3,16 +3,28 @@ namespace Plugin.Firebase.Storage.Platforms.iOS; +/// +/// Wraps a native iOS Firebase StorageListResult to implement IStorageListResult. +/// public sealed class StorageListResultWrapper : IStorageListResult { private readonly StorageListResult _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS storage list result to wrap. public StorageListResultWrapper(StorageListResult storageListResult) { _wrapped = storageListResult; } + /// public IEnumerable Items => _wrapped.Items.Select(x => x.ToAbstract()); + + /// public IEnumerable Prefixes => _wrapped.Prefixes.Select(x => x.ToAbstract()); + + /// public string PageToken => _wrapped.PageToken; } \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/StorageReferenceWrapper.cs b/src/Storage/Platforms/iOS/StorageReferenceWrapper.cs index 50511651..09170aec 100644 --- a/src/Storage/Platforms/iOS/StorageReferenceWrapper.cs +++ b/src/Storage/Platforms/iOS/StorageReferenceWrapper.cs @@ -5,20 +5,29 @@ namespace Plugin.Firebase.Storage.Platforms.iOS; +/// +/// Wraps a native iOS Firebase StorageReference to implement IStorageReference. +/// public sealed class StorageReferenceWrapper : IStorageReference { private readonly StorageReference _wrapped; + /// + /// Initializes a new instance of the class. + /// + /// The native iOS storage reference to wrap. public StorageReferenceWrapper(StorageReference reference) { _wrapped = reference; } + /// public IStorageReference GetChild(string path) { return _wrapped.GetChild(path).ToAbstract(); } + /// public IStorageTransferTask PutBytes(byte[] bytes, IStorageMetadata metadata = null) { return PutData(NSData.FromArray(bytes), metadata); @@ -27,104 +36,145 @@ public IStorageTransferTask PutBytes(byte[] bytes, IStorageMetadata metadata = n private IStorageTransferTask PutData(NSData data, IStorageMetadata metadata = null) { var wrapper = new StorageTransferTaskWrapper(); - wrapper.TransferTask = _wrapped.PutData(data, metadata?.ToNative(), (x, e) => wrapper.CompletionHandler(x, e)); + wrapper.TransferTask = _wrapped.PutData( + data, + metadata?.ToNative(), + (x, e) => wrapper.CompletionHandler(x, e) + ); return wrapper; } + /// public IStorageTransferTask PutFile(string filePath, IStorageMetadata metadata = null) { return PutData(NSData.FromStream(File.Open(filePath, FileMode.Open)), metadata); } + /// public IStorageTransferTask PutStream(Stream stream, IStorageMetadata metadata = null) { return PutData(NSData.FromStream(stream), metadata); } + /// public async Task GetMetadataAsync() { return (await _wrapped.GetMetadataAsync()).ToAbstract(); } + /// public async Task UpdateMetadataAsync(IStorageMetadata metadata) { return (await _wrapped.UpdateMetadataAsync(metadata.ToNative())).ToAbstract(); } + /// public async Task GetDownloadUrlAsync() { var uri = await _wrapped.GetDownloadUrlAsync(); return uri.AbsoluteString; } + /// public Task ListAsync(long maxResults) { var tcs = new TaskCompletionSource(); - _wrapped.List(maxResults, (listResult, error) => { - if(error == null) { - tcs.SetResult(listResult.ToAbstract()); - } else { - tcs.SetException(new FirebaseException(error.LocalizedDescription)); + _wrapped.List( + maxResults, + (listResult, error) => { + if(error == null) { + tcs.SetResult(listResult.ToAbstract()); + } else { + tcs.SetException(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return tcs.Task; } + /// public Task ListAllAsync() { var tcs = new TaskCompletionSource(); - _wrapped.ListAll((x, error) => { - if(error == null) { - tcs.SetResult(x.ToAbstract()); - } else { - tcs.SetException(new FirebaseException(error.LocalizedDescription)); + _wrapped.ListAll( + (x, error) => { + if(error == null) { + tcs.SetResult(x.ToAbstract()); + } else { + tcs.SetException(new FirebaseException(error.LocalizedDescription)); + } } - }); + ); return tcs.Task; } + /// public Task GetStreamAsync(long maxSize) { var tcs = new TaskCompletionSource(); - _wrapped.GetData(maxSize, (data, error) => { - if(error == null && data != null) { - tcs.SetResult(data.AsStream()); - } else { - tcs.SetException(new FirebaseException(error?.LocalizedDescription ?? "Data is null")); + _wrapped.GetData( + maxSize, + (data, error) => { + if(error == null && data != null) { + tcs.SetResult(data.AsStream()); + } else { + tcs.SetException( + new FirebaseException(error?.LocalizedDescription ?? "Data is null") + ); + } } - }); + ); return tcs.Task; } - + /// public Task GetBytesAsync(long maxDownloadSizeBytes) { var tcs = new TaskCompletionSource(); - _wrapped.GetData(maxDownloadSizeBytes, (data, error) => { - if(error == null && data != null) { - tcs.SetResult(data.ToArray()); - } else { - tcs.SetException(new FirebaseException(error?.LocalizedDescription ?? "Data is null")); + _wrapped.GetData( + maxDownloadSizeBytes, + (data, error) => { + if(error == null && data != null) { + tcs.SetResult(data.ToArray()); + } else { + tcs.SetException( + new FirebaseException(error?.LocalizedDescription ?? "Data is null") + ); + } } - }); + ); return tcs.Task; } + /// public IStorageTransferTask DownloadFile(string destinationPath) { var wrapper = new StorageTransferTaskWrapper(); - wrapper.TransferTask = _wrapped.WriteToFile(NSUrl.FromFilename(destinationPath), (x, e) => wrapper.CompletionHandler(x, e)); + wrapper.TransferTask = _wrapped.WriteToFile( + NSUrl.FromFilename(destinationPath), + (x, e) => wrapper.CompletionHandler(x, e) + ); return wrapper; } + /// public Task DeleteAsync() { return _wrapped.DeleteAsync(); } + /// public IStorageReference Parent => _wrapped.Parent?.ToAbstract(); + + /// public IStorageReference Root => _wrapped.Root.ToAbstract(); + + /// public string Bucket => _wrapped.Bucket; + + /// public string Name => _wrapped.Name; + + /// public string FullPath => $"/{_wrapped.FullPath}"; -} +} \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/StorageTaskSnapshotWrapper.cs b/src/Storage/Platforms/iOS/StorageTaskSnapshotWrapper.cs index 609b9beb..d3441c95 100644 --- a/src/Storage/Platforms/iOS/StorageTaskSnapshotWrapper.cs +++ b/src/Storage/Platforms/iOS/StorageTaskSnapshotWrapper.cs @@ -3,13 +3,26 @@ namespace Plugin.Firebase.Storage.Platforms.iOS; +/// +/// Wraps a native iOS Firebase StorageTaskSnapshot to implement IStorageTaskSnapshot. +/// public class StorageTaskTaskSnapshotWrapper : IStorageTaskSnapshot { + /// + /// Creates a new snapshot wrapper from a native iOS storage task snapshot. + /// + /// The native snapshot to wrap. + /// An abstract storage task snapshot. public static IStorageTaskSnapshot FromSnapshot(StorageTaskSnapshot snapshot) { return new StorageTaskTaskSnapshotWrapper(snapshot: snapshot); } + /// + /// Creates a new snapshot wrapper representing an error state. + /// + /// The exception that occurred. + /// An abstract storage task snapshot with the error. public static IStorageTaskSnapshot FromError(Exception error) { return new StorageTaskTaskSnapshotWrapper(error: error); @@ -17,7 +30,8 @@ public static IStorageTaskSnapshot FromError(Exception error) private StorageTaskTaskSnapshotWrapper( StorageTaskSnapshot snapshot = null, - Exception error = null) + Exception error = null + ) { if(snapshot?.Progress != null) { TransferredUnitCount = snapshot.Progress.CompletedUnitCount; @@ -29,9 +43,18 @@ private StorageTaskTaskSnapshotWrapper( Error = error; } + /// public long TransferredUnitCount { get; } + + /// public long TotalUnitCount { get; } + + /// public double TransferredFraction { get; } + + /// public IStorageMetadata Metadata { get; } + + /// public Exception Error { get; } } \ No newline at end of file diff --git a/src/Storage/Platforms/iOS/StorageTransferTaskWrapper.cs b/src/Storage/Platforms/iOS/StorageTransferTaskWrapper.cs index d2b51f28..a0b5aaec 100644 --- a/src/Storage/Platforms/iOS/StorageTransferTaskWrapper.cs +++ b/src/Storage/Platforms/iOS/StorageTransferTaskWrapper.cs @@ -3,12 +3,21 @@ namespace Plugin.Firebase.Storage.Platforms.iOS; -public sealed class StorageTransferTaskWrapper : IStorageTransferTask +/// +/// Wraps a native iOS Firebase storage transfer task to implement IStorageTransferTask. +/// +/// The native iOS storage transfer task type. +/// The completion result type. +public sealed class StorageTransferTaskWrapper + : IStorageTransferTask where TStorageTransferTask : StorageObservableTask, IStorageTaskManagement { private readonly TaskCompletionSource _tcs; private readonly IDictionary, string> _observerDict; + /// + /// Initializes a new instance of the class. + /// public StorageTransferTaskWrapper() { _tcs = new TaskCompletionSource(); @@ -23,21 +32,29 @@ public StorageTransferTaskWrapper() }; } + /// public Task AwaitAsync() { return _tcs.Task; } + /// public void AddObserver(StorageTaskStatus status, Action observer) { if(TransferTask == null) { - throw new ArgumentException($"You have to set the {nameof(TransferTask)} property before calling this method"); + throw new ArgumentException( + $"You have to set the {nameof(TransferTask)} property before calling this method" + ); } - var handle = TransferTask.ObserveStatus(status.ToNative(), x => observer.Invoke(x.ToAbstract())); + var handle = TransferTask.ObserveStatus( + status.ToNative(), + x => observer.Invoke(x.ToAbstract()) + ); _observerDict[observer] = handle; } + /// public void RemoveObserver(Action observer) { if(_observerDict.ContainsKey(observer)) { @@ -46,22 +63,38 @@ public void RemoveObserver(Action observer) } } + /// public void Pause() { TransferTask.Pause(); } + /// public void Resume() { TransferTask.Resume(); } + /// public void Cancel() { TransferTask.Cancel(); } + /// + /// Delegate for handling storage transfer completion. + /// + /// The completion result. + /// The error if the transfer failed, otherwise null. public delegate void StorageTransferCompletionHandler(TCompletionResult result, NSError error); + + /// + /// Gets the completion handler for the transfer task. + /// public StorageTransferCompletionHandler CompletionHandler { get; } + + /// + /// Gets or sets the underlying native transfer task. + /// public TStorageTransferTask TransferTask { private get; set; } } \ No newline at end of file diff --git a/src/Storage/Shared/CrossFirebaseStorage.cs b/src/Storage/Shared/CrossFirebaseStorage.cs index a63bc77f..5ec3e1ca 100644 --- a/src/Storage/Shared/CrossFirebaseStorage.cs +++ b/src/Storage/Shared/CrossFirebaseStorage.cs @@ -1,8 +1,14 @@ namespace Plugin.Firebase.Storage; +/// +/// Cross-platform entry point for Firebase Storage. +/// public sealed class CrossFirebaseStorage { - private static Lazy _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + private static Lazy _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); private static IFirebaseStorage CreateInstance() { @@ -34,16 +40,21 @@ public static IFirebaseStorage Current { } private static Exception NotImplementedInReferenceAssembly() => - new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation."); + new NotImplementedException( + "This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation." + ); /// - /// Dispose of everything + /// Dispose of everything /// public static void Dispose() { if(_implementation != null && _implementation.IsValueCreated) { _implementation.Value.Dispose(); - _implementation = new Lazy(CreateInstance, LazyThreadSafetyMode.PublicationOnly); + _implementation = new Lazy( + CreateInstance, + LazyThreadSafetyMode.PublicationOnly + ); } } } \ No newline at end of file diff --git a/src/Storage/Shared/IStorageListResult.cs b/src/Storage/Shared/IStorageListResult.cs index e4ea8c19..73fb0f78 100644 --- a/src/Storage/Shared/IStorageListResult.cs +++ b/src/Storage/Shared/IStorageListResult.cs @@ -1,5 +1,8 @@ namespace Plugin.Firebase.Storage; +/// +/// Represents the result of a storage list operation. +/// public interface IStorageListResult { /// diff --git a/src/Storage/Shared/IStorageTransferTask.cs b/src/Storage/Shared/IStorageTransferTask.cs index d5b205e7..64deb0d4 100644 --- a/src/Storage/Shared/IStorageTransferTask.cs +++ b/src/Storage/Shared/IStorageTransferTask.cs @@ -5,6 +5,10 @@ namespace Plugin.Firebase.Storage; /// public interface IStorageTransferTask { + /// + /// Waits for the transfer task to complete. + /// + /// A task that completes when the transfer is finished. Task AwaitAsync(); /// @@ -23,17 +27,17 @@ public interface IStorageTransferTask void RemoveObserver(Action observer); /// - /// Attempts to pause the task. + /// Attempts to pause the task. /// void Pause(); /// - /// Attempts to resume a paused task. + /// Attempts to resume a paused task. /// void Resume(); /// - /// Attempts to cancel the task. + /// Attempts to cancel the task. /// void Cancel(); } \ No newline at end of file diff --git a/src/Storage/Shared/StorageMetadata.cs b/src/Storage/Shared/StorageMetadata.cs index 2d3d24eb..50b47bc5 100644 --- a/src/Storage/Shared/StorageMetadata.cs +++ b/src/Storage/Shared/StorageMetadata.cs @@ -1,7 +1,29 @@ namespace Plugin.Firebase.Storage; +/// +/// Represents metadata for an object stored in Firebase Storage. +/// public sealed class StorageMetadata : IStorageMetadata { + /// + /// Creates a new StorageMetadata instance. + /// + /// The name of the bucket containing this object. + /// The content generation of this object. + /// The version of the metadata for this object. + /// The name of this object. + /// The full path of this object. + /// Content-Length of the data in bytes. + /// Cache-Control directive for the object data. + /// Content-Disposition of the object data. + /// Content-Encoding of the object data. + /// Content-Language of the object data. + /// Content-Type of the object data. + /// User-provided metadata in key/value pairs. + /// MD5 hash of the data, encoded using base64. + /// A reference to the object in Firebase Storage. + /// The time the object was last updated. + /// The time the object was created. public StorageMetadata( string bucket = null, long generation = 0, @@ -18,7 +40,8 @@ public StorageMetadata( string md5Hash = null, IStorageReference storageReference = null, DateTimeOffset updatedTime = default(DateTimeOffset), - DateTimeOffset creationTime = default(DateTimeOffset)) + DateTimeOffset creationTime = default(DateTimeOffset) + ) { Bucket = bucket; Generation = generation; @@ -38,25 +61,57 @@ public StorageMetadata( UpdatedTime = creationTime; } + /// public override string ToString() { return $"[{nameof(StorageMetadata)}: {nameof(Path)}={Path}, {nameof(ContentType)}={ContentType}, {nameof(Size)}={Size}]"; } + /// public string Bucket { get; } + + /// public long Generation { get; } + + /// public long MetaGeneration { get; } + + /// public string Name { get; } + + /// public string Path { get; } + + /// public long Size { get; } + + /// public string CacheControl { get; } + + /// public string ContentDisposition { get; } + + /// public string ContentEncoding { get; } + + /// public string ContentLanguage { get; } + + /// public string ContentType { get; } + + /// public IDictionary CustomMetadata { get; } + + /// public string MD5Hash { get; } + + /// public IStorageReference StorageReference { get; } + + /// public DateTimeOffset CreationTime { get; } + + /// public DateTimeOffset UpdatedTime { get; } } \ No newline at end of file diff --git a/src/Storage/Shared/StorageTaskStatus.cs b/src/Storage/Shared/StorageTaskStatus.cs index c9254859..7068d6f9 100644 --- a/src/Storage/Shared/StorageTaskStatus.cs +++ b/src/Storage/Shared/StorageTaskStatus.cs @@ -1,10 +1,32 @@ namespace Plugin.Firebase.Storage; +/// +/// Represents the status of a storage transfer task. +/// public enum StorageTaskStatus : long { + /// + /// The task status is unknown. + /// Unknown, + + /// + /// The task is in progress. + /// Progress, + + /// + /// The task is paused. + /// Pause, + + /// + /// The task completed successfully. + /// Success, - Failure + + /// + /// The task failed. + /// + Failure, } \ No newline at end of file diff --git a/src/Storage/Storage.csproj b/src/Storage/Storage.csproj index 1cadeef7..320875d3 100644 --- a/src/Storage/Storage.csproj +++ b/src/Storage/Storage.csproj @@ -22,7 +22,7 @@ Plugin.Firebase.Storage - 4.0.1 + 4.0.2 MIT https://github.com/TobiasBuchholz/Plugin.Firebase diff --git a/tests/Plugin.Firebase.IntegrationTests/Plugin.Firebase.IntegrationTests.csproj b/tests/Plugin.Firebase.IntegrationTests/Plugin.Firebase.IntegrationTests.csproj index 8bc58649..44e4054a 100644 --- a/tests/Plugin.Firebase.IntegrationTests/Plugin.Firebase.IntegrationTests.csproj +++ b/tests/Plugin.Firebase.IntegrationTests/Plugin.Firebase.IntegrationTests.csproj @@ -40,6 +40,7 @@ Platforms\iOS\Entitlements.plist + false @@ -56,7 +57,21 @@ - + + + + + + + + + + + + + + +