Add Jarvis Google Calendar OAuth broker
This commit is contained in:
+375
-2
@@ -1,9 +1,12 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -151,6 +154,214 @@ app.MapGet("/api/app-config", async (HttpContext context, OtthonDbContext db) =>
|
||||
!string.IsNullOrWhiteSpace(googleClientId)));
|
||||
});
|
||||
|
||||
app.MapGet("/api/google/oauth/start", async (
|
||||
HttpContext context,
|
||||
OtthonDbContext db) =>
|
||||
{
|
||||
if (!HasApiToken(context))
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var workspaceToken = GetWorkspaceToken(context);
|
||||
if (string.IsNullOrWhiteSpace(workspaceToken))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Missing X-Workspace-Token header." });
|
||||
}
|
||||
|
||||
var clientId = await GetGoogleClientId(db);
|
||||
var clientSecret = await GetGoogleClientSecret(db);
|
||||
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Google OAuth client is not configured." });
|
||||
}
|
||||
|
||||
var stateValue = RandomToken(32);
|
||||
db.GoogleOAuthStates.Add(new GoogleOAuthState
|
||||
{
|
||||
State = stateValue,
|
||||
WorkspaceToken = workspaceToken,
|
||||
CreatedUtc = DateTimeOffset.UtcNow,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(15)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var redirectUri = GoogleOAuthRedirectUri();
|
||||
var authUri = QueryHelpers.AddQueryString("https://accounts.google.com/o/oauth2/v2/auth", new Dictionary<string, string?>
|
||||
{
|
||||
["client_id"] = clientId,
|
||||
["redirect_uri"] = redirectUri,
|
||||
["response_type"] = "code",
|
||||
["scope"] = "openid email https://www.googleapis.com/auth/calendar.readonly",
|
||||
["access_type"] = "offline",
|
||||
["prompt"] = "consent",
|
||||
["state"] = stateValue
|
||||
});
|
||||
|
||||
return Results.Ok(new GoogleOAuthStartResponse(authUri));
|
||||
});
|
||||
|
||||
app.MapGet("/api/google/oauth/callback", async (
|
||||
string? code,
|
||||
string? state,
|
||||
string? error,
|
||||
OtthonDbContext db,
|
||||
IHttpClientFactory httpClientFactory) =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, $"Google rejected the request: {Html(error)}"), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(state))
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, "Missing OAuth code or state."), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
var savedState = await db.GoogleOAuthStates.FindAsync(state);
|
||||
if (savedState is null || savedState.ExpiresUtc < DateTimeOffset.UtcNow)
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, "OAuth session expired. Start Google connection again from the tablet."), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
var clientId = await GetGoogleClientId(db);
|
||||
var clientSecret = await GetGoogleClientSecret(db);
|
||||
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, "Google OAuth client is not configured in JARVIS."), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
var http = httpClientFactory.CreateClient();
|
||||
var tokenResponse = await http.PostAsync(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["code"] = code,
|
||||
["client_id"] = clientId,
|
||||
["client_secret"] = clientSecret,
|
||||
["redirect_uri"] = GoogleOAuthRedirectUri(),
|
||||
["grant_type"] = "authorization_code"
|
||||
}));
|
||||
var tokenBody = await tokenResponse.Content.ReadAsStringAsync();
|
||||
if (!tokenResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, $"Token exchange failed: {Html(tokenBody)}"), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
using var tokenJson = JsonDocument.Parse(tokenBody);
|
||||
var accessToken = GetString(tokenJson.RootElement, "access_token");
|
||||
var refreshToken = GetString(tokenJson.RootElement, "refresh_token");
|
||||
var expiresIn = tokenJson.RootElement.TryGetProperty("expires_in", out var expiresElement)
|
||||
? expiresElement.GetInt32()
|
||||
: 3600;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
return Results.Content(GoogleOAuthResultPage(false, "Google did not return an access token."), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
var email = await LoadGoogleEmail(http, accessToken);
|
||||
var auth = await db.GoogleCalendarAuths.FindAsync(savedState.WorkspaceToken);
|
||||
if (auth is null)
|
||||
{
|
||||
auth = new GoogleCalendarAuth
|
||||
{
|
||||
WorkspaceToken = savedState.WorkspaceToken,
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken ?? "",
|
||||
Email = email,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(Math.Max(60, expiresIn - 60)),
|
||||
UpdatedUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
db.GoogleCalendarAuths.Add(auth);
|
||||
}
|
||||
else
|
||||
{
|
||||
auth.AccessToken = accessToken;
|
||||
if (!string.IsNullOrWhiteSpace(refreshToken))
|
||||
{
|
||||
auth.RefreshToken = refreshToken;
|
||||
}
|
||||
auth.Email = email ?? auth.Email;
|
||||
auth.ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(Math.Max(60, expiresIn - 60));
|
||||
auth.UpdatedUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
db.GoogleOAuthStates.Remove(savedState);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Content(GoogleOAuthResultPage(true, $"Google Calendar connected for {Html(auth.Email ?? "the selected account")}."), "text/html; charset=utf-8");
|
||||
});
|
||||
|
||||
app.MapGet("/api/google/oauth/status", async (HttpContext context, OtthonDbContext db) =>
|
||||
{
|
||||
if (!HasApiToken(context)) return Results.Unauthorized();
|
||||
var workspaceToken = GetWorkspaceToken(context);
|
||||
if (string.IsNullOrWhiteSpace(workspaceToken))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Missing X-Workspace-Token header." });
|
||||
}
|
||||
var auth = await db.GoogleCalendarAuths.FindAsync(workspaceToken);
|
||||
return Results.Ok(new GoogleOAuthStatusResponse(auth is not null, auth?.Email, auth?.UpdatedUtc));
|
||||
});
|
||||
|
||||
app.MapGet("/api/google/calendars", async (
|
||||
HttpContext context,
|
||||
OtthonDbContext db,
|
||||
IHttpClientFactory httpClientFactory) =>
|
||||
{
|
||||
if (!HasApiToken(context)) return Results.Unauthorized();
|
||||
var workspaceToken = GetWorkspaceToken(context);
|
||||
if (string.IsNullOrWhiteSpace(workspaceToken))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Missing X-Workspace-Token header." });
|
||||
}
|
||||
var token = await GetGoogleAccessToken(db, httpClientFactory, workspaceToken);
|
||||
if (token is null) return Results.BadRequest(new { message = "Google Calendar is not connected." });
|
||||
|
||||
var http = httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/calendar/v3/users/me/calendarList?minAccessRole=reader&showHidden=true");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await http.SendAsync(request);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
return Results.Content(body, "application/json; charset=utf-8", statusCode: (int)response.StatusCode);
|
||||
});
|
||||
|
||||
app.MapGet("/api/google/events", async (
|
||||
HttpContext context,
|
||||
string calendarId,
|
||||
string? timeMin,
|
||||
string? timeMax,
|
||||
OtthonDbContext db,
|
||||
IHttpClientFactory httpClientFactory) =>
|
||||
{
|
||||
if (!HasApiToken(context)) return Results.Unauthorized();
|
||||
var workspaceToken = GetWorkspaceToken(context);
|
||||
if (string.IsNullOrWhiteSpace(workspaceToken))
|
||||
{
|
||||
return Results.BadRequest(new { message = "Missing X-Workspace-Token header." });
|
||||
}
|
||||
var token = await GetGoogleAccessToken(db, httpClientFactory, workspaceToken);
|
||||
if (token is null) return Results.BadRequest(new { message = "Google Calendar is not connected." });
|
||||
|
||||
var query = new Dictionary<string, string?>
|
||||
{
|
||||
["singleEvents"] = "true",
|
||||
["orderBy"] = "startTime",
|
||||
["timeMin"] = timeMin,
|
||||
["timeMax"] = timeMax,
|
||||
["maxResults"] = "2500"
|
||||
};
|
||||
var uri = QueryHelpers.AddQueryString(
|
||||
$"https://www.googleapis.com/calendar/v3/calendars/{Uri.EscapeDataString(calendarId)}/events",
|
||||
query);
|
||||
var http = httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await http.SendAsync(request);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
return Results.Content(body, "application/json; charset=utf-8", statusCode: (int)response.StatusCode);
|
||||
});
|
||||
|
||||
app.MapPost("/api/devices/register", async (
|
||||
DeviceRegistrationRequest request,
|
||||
HttpContext context,
|
||||
@@ -661,6 +872,88 @@ static async Task SetSetting(OtthonDbContext db, string key, string value)
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static string RandomToken(int byteCount)
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(byteCount);
|
||||
return Convert.ToBase64String(bytes)
|
||||
.Replace("+", "-")
|
||||
.Replace("/", "_")
|
||||
.TrimEnd('=');
|
||||
}
|
||||
|
||||
static string GoogleOAuthRedirectUri() =>
|
||||
"https://api-test.a-web.hu/api/google/oauth/callback";
|
||||
|
||||
static string GoogleCalendarServerClientIdSettingKey() => "GoogleCalendarServerClientId";
|
||||
static string GoogleCalendarClientSecretSettingKey() => "GoogleCalendarClientSecret";
|
||||
|
||||
static async Task<string?> GetGoogleClientId(OtthonDbContext db) =>
|
||||
await GetSetting(db, GoogleCalendarServerClientIdSettingKey());
|
||||
|
||||
static async Task<string?> GetGoogleClientSecret(OtthonDbContext db) =>
|
||||
await GetSetting(db, GoogleCalendarClientSecretSettingKey());
|
||||
|
||||
static async Task<string?> LoadGoogleEmail(HttpClient http, string accessToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/oauth2/v2/userinfo");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
var response = await http.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
using var json = JsonDocument.Parse(body);
|
||||
return GetString(json.RootElement, "email");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string?> GetGoogleAccessToken(
|
||||
OtthonDbContext db,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
string workspaceToken)
|
||||
{
|
||||
var auth = await db.GoogleCalendarAuths.FindAsync(workspaceToken);
|
||||
if (auth is null) return null;
|
||||
if (auth.ExpiresUtc > DateTimeOffset.UtcNow.AddMinutes(2))
|
||||
{
|
||||
return auth.AccessToken;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.RefreshToken)) return null;
|
||||
|
||||
var clientId = await GetGoogleClientId(db);
|
||||
var clientSecret = await GetGoogleClientSecret(db);
|
||||
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret)) return null;
|
||||
|
||||
var http = httpClientFactory.CreateClient();
|
||||
var response = await http.PostAsync(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["client_id"] = clientId,
|
||||
["client_secret"] = clientSecret,
|
||||
["refresh_token"] = auth.RefreshToken,
|
||||
["grant_type"] = "refresh_token"
|
||||
}));
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
using var json = JsonDocument.Parse(body);
|
||||
var accessToken = GetString(json.RootElement, "access_token");
|
||||
if (string.IsNullOrWhiteSpace(accessToken)) return null;
|
||||
var expiresIn = json.RootElement.TryGetProperty("expires_in", out var expiresElement)
|
||||
? expiresElement.GetInt32()
|
||||
: 3600;
|
||||
auth.AccessToken = accessToken;
|
||||
auth.ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(Math.Max(60, expiresIn - 60));
|
||||
auth.UpdatedUtc = DateTimeOffset.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return auth.AccessToken;
|
||||
}
|
||||
|
||||
static void ApplyProductLookup(Product target, Product lookup, string source)
|
||||
{
|
||||
target.Name = lookup.Name;
|
||||
@@ -747,6 +1040,7 @@ static async Task EnsureDatabaseReady(OtthonDbContext db, ILogger logger)
|
||||
await EnsureFoodLearningSchemaAsync(db);
|
||||
await EnsureWorkspaceSchemaAsync(db);
|
||||
await EnsureAppSettingsSchemaAsync(db);
|
||||
await EnsureGoogleCalendarSchemaAsync(db);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (attempt < maxAttempts)
|
||||
@@ -765,6 +1059,7 @@ static async Task EnsureDatabaseReady(OtthonDbContext db, ILogger logger)
|
||||
await EnsureFoodLearningSchemaAsync(db);
|
||||
await EnsureWorkspaceSchemaAsync(db);
|
||||
await EnsureAppSettingsSchemaAsync(db);
|
||||
await EnsureGoogleCalendarSchemaAsync(db);
|
||||
}
|
||||
|
||||
static async Task EnsureProductSchemaAsync(OtthonDbContext db)
|
||||
@@ -815,6 +1110,33 @@ END
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
}
|
||||
|
||||
static async Task EnsureGoogleCalendarSchemaAsync(OtthonDbContext db)
|
||||
{
|
||||
var sql = """
|
||||
IF OBJECT_ID('GoogleCalendarAuths', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE GoogleCalendarAuths (
|
||||
WorkspaceToken nvarchar(160) NOT NULL CONSTRAINT PK_GoogleCalendarAuths PRIMARY KEY,
|
||||
Email nvarchar(320) NULL,
|
||||
AccessToken nvarchar(max) NOT NULL,
|
||||
RefreshToken nvarchar(max) NOT NULL,
|
||||
ExpiresUtc datetimeoffset NOT NULL,
|
||||
UpdatedUtc datetimeoffset NOT NULL
|
||||
);
|
||||
END
|
||||
IF OBJECT_ID('GoogleOAuthStates', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE GoogleOAuthStates (
|
||||
[State] nvarchar(160) NOT NULL CONSTRAINT PK_GoogleOAuthStates PRIMARY KEY,
|
||||
WorkspaceToken nvarchar(160) NOT NULL,
|
||||
CreatedUtc datetimeoffset NOT NULL,
|
||||
ExpiresUtc datetimeoffset NOT NULL
|
||||
);
|
||||
END
|
||||
""";
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
}
|
||||
|
||||
static async Task EnsureFoodLearningSchemaAsync(OtthonDbContext db)
|
||||
{
|
||||
var sql = """
|
||||
@@ -1047,9 +1369,27 @@ static string SettingsPage(string googleClientId) => $$"""
|
||||
</html>
|
||||
""";
|
||||
|
||||
static string Html(string? value) => System.Net.WebUtility.HtmlEncode(value ?? "");
|
||||
static string GoogleOAuthResultPage(bool success, string message) => $$"""
|
||||
<!doctype html>
|
||||
<html lang="hu">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Google Calendar connection</title>
|
||||
<style>{{AdminCss()}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card narrow">
|
||||
<p class="eyebrow">JARVIS API</p>
|
||||
<h1>{{(success ? "Google Calendar connected" : "Google Calendar error")}}</h1>
|
||||
<p class="{{(success ? "hint" : "error")}}">{{message}}</p>
|
||||
<p>You can close this browser window and return to Közös Tér.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
static string GoogleCalendarServerClientIdSettingKey() => "GoogleCalendarServerClientId";
|
||||
static string Html(string? value) => System.Net.WebUtility.HtmlEncode(value ?? "");
|
||||
|
||||
static string AdminCss() => """
|
||||
:root { color-scheme: light; font-family: Inter, Segoe UI, Arial, sans-serif; }
|
||||
@@ -1091,11 +1431,19 @@ public sealed class OtthonDbContext(DbContextOptions<OtthonDbContext> options)
|
||||
public DbSet<FoodLearningEvent> FoodLearningEvents => Set<FoodLearningEvent>();
|
||||
public DbSet<WorkspaceState> Workspaces => Set<WorkspaceState>();
|
||||
public DbSet<AppSetting> AppSettings => Set<AppSetting>();
|
||||
public DbSet<GoogleCalendarAuth> GoogleCalendarAuths => Set<GoogleCalendarAuth>();
|
||||
public DbSet<GoogleOAuthState> GoogleOAuthStates => Set<GoogleOAuthState>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<AppSetting>().HasKey(setting => setting.Key);
|
||||
modelBuilder.Entity<AppSetting>().Property(setting => setting.Key).HasMaxLength(160);
|
||||
modelBuilder.Entity<GoogleCalendarAuth>().HasKey(auth => auth.WorkspaceToken);
|
||||
modelBuilder.Entity<GoogleCalendarAuth>().Property(auth => auth.WorkspaceToken).HasMaxLength(160);
|
||||
modelBuilder.Entity<GoogleCalendarAuth>().Property(auth => auth.Email).HasMaxLength(320);
|
||||
modelBuilder.Entity<GoogleOAuthState>().HasKey(state => state.State);
|
||||
modelBuilder.Entity<GoogleOAuthState>().Property(state => state.State).HasMaxLength(160);
|
||||
modelBuilder.Entity<GoogleOAuthState>().Property(state => state.WorkspaceToken).HasMaxLength(160);
|
||||
modelBuilder.Entity<WorkspaceState>().HasKey(workspace => workspace.Token);
|
||||
modelBuilder.Entity<WorkspaceState>().Property(workspace => workspace.Token).HasMaxLength(160);
|
||||
modelBuilder.Entity<Product>().HasKey(product => product.Barcode);
|
||||
@@ -1177,6 +1525,24 @@ public sealed class AppSetting
|
||||
public DateTimeOffset UpdatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class GoogleCalendarAuth
|
||||
{
|
||||
public required string WorkspaceToken { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public required string AccessToken { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
public DateTimeOffset ExpiresUtc { get; set; }
|
||||
public DateTimeOffset UpdatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class GoogleOAuthState
|
||||
{
|
||||
public required string State { get; set; }
|
||||
public required string WorkspaceToken { get; set; }
|
||||
public DateTimeOffset CreatedUtc { get; set; }
|
||||
public DateTimeOffset ExpiresUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed record LoginForm(string Password);
|
||||
public sealed record GoogleCalendarSettingsForm(string? ServerClientId);
|
||||
public sealed record DeviceRegistrationRequest(string? DeviceName, string? Platform);
|
||||
@@ -1252,6 +1618,13 @@ public sealed record AppConfigResponse(
|
||||
string? GoogleCalendarServerClientId,
|
||||
bool GoogleCalendarConfigured);
|
||||
|
||||
public sealed record GoogleOAuthStartResponse(string AuthorizationUrl);
|
||||
|
||||
public sealed record GoogleOAuthStatusResponse(
|
||||
bool Connected,
|
||||
string? Email,
|
||||
DateTimeOffset? UpdatedUtc);
|
||||
|
||||
public sealed record ProductResponse(
|
||||
bool Found,
|
||||
string Barcode,
|
||||
|
||||
+115
-182
@@ -25,15 +25,7 @@ class AccountsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountsPageState extends State<AccountsPage> {
|
||||
static const calendarScopes = [
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
];
|
||||
|
||||
final googleSignIn = GoogleSignIn.instance;
|
||||
List<GoogleCalendarInfo> googleCalendars = [];
|
||||
GoogleSignInAccount? signedInAccount;
|
||||
String? runtimeServerClientId;
|
||||
bool initialized = false;
|
||||
bool loadingCalendars = false;
|
||||
bool syncing = false;
|
||||
String? statusMessage;
|
||||
@@ -41,209 +33,156 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGoogleAppConfigAndInitialize();
|
||||
_loadGoogleConnectionStatus();
|
||||
}
|
||||
|
||||
GoogleCalendarConnection? get connection =>
|
||||
widget.connections.isEmpty ? null : widget.connections.first;
|
||||
|
||||
Future<void> _loadGoogleAppConfigAndInitialize() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google napt\u00e1rkapcsolat el\u0151k\u00e9sz\u00edt\u00e9se...';
|
||||
});
|
||||
|
||||
runtimeServerClientId = widget.serverClientId.trim().isNotEmpty
|
||||
? widget.serverClientId.trim()
|
||||
: await _loadGoogleServerClientId();
|
||||
if (!mounted) return;
|
||||
await _initializeGoogle();
|
||||
String? get _workspaceToken {
|
||||
final token = widget.jarvisSettings.workspaceToken?.trim();
|
||||
return token == null || token.isEmpty ? null : token;
|
||||
}
|
||||
|
||||
Future<String?> _loadGoogleServerClientId() async {
|
||||
if (googleCalendarServerClientId.trim().isNotEmpty) {
|
||||
return googleCalendarServerClientId.trim();
|
||||
Map<String, String> get _jarvisHeaders {
|
||||
final headers = <String, String>{
|
||||
'X-Otthon-Token': widget.jarvisSettings.apiToken.trim(),
|
||||
};
|
||||
final workspaceToken = _workspaceToken;
|
||||
if (workspaceToken != null) {
|
||||
headers['X-Workspace-Token'] = workspaceToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
final apiToken = widget.jarvisSettings.apiToken.trim();
|
||||
if (apiToken.isEmpty) {
|
||||
return null;
|
||||
Uri _jarvisUri(String path, [Map<String, String?> query = const {}]) {
|
||||
final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl));
|
||||
return baseUri.replace(path: path, queryParameters: query.isEmpty ? null : query);
|
||||
}
|
||||
|
||||
String? _jarvisConfigError() {
|
||||
if (widget.jarvisSettings.apiToken.trim().isEmpty) {
|
||||
return 'Add meg a JARVIS API tokent a fogasker\u00e9k alatt.';
|
||||
}
|
||||
if (_workspaceToken == null) {
|
||||
return 'Add meg a k\u00f6z\u00f6s munkat\u00e9r tokent a fogasker\u00e9k alatt.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _loadGoogleConnectionStatus() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'JARVIS Google kapcsolat ellen\u0151rz\u00e9se...';
|
||||
});
|
||||
|
||||
final configError = _jarvisConfigError();
|
||||
if (configError != null) {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = configError;
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final baseUri = Uri.parse(normalizeJarvisUrl(widget.jarvisSettings.apiUrl));
|
||||
final uri = baseUri.replace(path: '/api/app-config', query: '');
|
||||
final response = await http
|
||||
.get(uri, headers: {'X-Otthon-Token': apiToken})
|
||||
.get(_jarvisUri('/api/google/oauth/status'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
throw Exception('JARVIS status ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
final payload = jsonDecode(response.body);
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
return null;
|
||||
throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.');
|
||||
}
|
||||
return payload['googleCalendarServerClientId'] as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeGoogle() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google kapcsolat el\u0151k\u00e9sz\u00edt\u00e9se...';
|
||||
});
|
||||
|
||||
try {
|
||||
await _ensureGoogleInitialized();
|
||||
initialized = true;
|
||||
final lightweight = googleSignIn.attemptLightweightAuthentication();
|
||||
final account = lightweight == null ? null : await lightweight;
|
||||
if (!mounted) return;
|
||||
if (account != null) {
|
||||
signedInAccount = account;
|
||||
await _loadGoogleCalendars(promptIfNecessary: false);
|
||||
final connected = payload['connected'] == true;
|
||||
final email = payload['email'] as String?;
|
||||
if (connected) {
|
||||
final current = connection;
|
||||
widget.onChanged([
|
||||
GoogleCalendarConnection(
|
||||
id: current?.id ?? uniqueId(),
|
||||
accountEmail: email ?? current?.accountEmail ?? 'Google fi\u00f3k',
|
||||
enabledCalendarIds: current?.enabledCalendarIds ?? const [],
|
||||
calendarPersonIds: current?.calendarPersonIds ?? const {},
|
||||
lastSync: current?.lastSync,
|
||||
),
|
||||
]);
|
||||
await _loadGoogleCalendars();
|
||||
} else {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage =
|
||||
'Jelentkezz be Google fi\u00f3kkal, majd v\u00e1laszd ki, mely napt\u00e1rak szinkroniz\u00e1ljanak.';
|
||||
'Google Calendar m\u00e9g nincs \u00f6sszek\u00f6tve. Ind\u00edtsd az \u00f6sszek\u00f6t\u00e9st JARVIS-on kereszt\u00fcl.';
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = _friendlyGoogleError(
|
||||
'Google inicializ\u00e1l\u00e1s nem siker\u00fclt',
|
||||
error,
|
||||
);
|
||||
statusMessage = 'JARVIS Google kapcsolat nem el\u00e9rhet\u0151: $error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _googleHeaders({
|
||||
bool promptIfNecessary = true,
|
||||
}) async {
|
||||
final account = signedInAccount;
|
||||
final headers = account == null
|
||||
? await googleSignIn.authorizationClient.authorizationHeaders(
|
||||
calendarScopes,
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
)
|
||||
: await account.authorizationClient.authorizationHeaders(
|
||||
calendarScopes,
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
);
|
||||
if (headers == null) {
|
||||
throw Exception('A Google Calendar jogosults\u00e1g nincs megadva.');
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
Future<void> _signInGoogle() async {
|
||||
Future<void> _startGoogleConnection() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google bejelentkez\u00e9s...';
|
||||
statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s ind\u00edt\u00e1sa JARVIS-on...';
|
||||
});
|
||||
|
||||
try {
|
||||
if (!initialized) {
|
||||
runtimeServerClientId = widget.serverClientId.trim().isNotEmpty
|
||||
? widget.serverClientId.trim()
|
||||
: await _loadGoogleServerClientId();
|
||||
await _ensureGoogleInitialized();
|
||||
initialized = true;
|
||||
}
|
||||
final account = await googleSignIn.authenticate();
|
||||
signedInAccount = account;
|
||||
await account.authorizationClient.authorizeScopes(calendarScopes);
|
||||
await _loadGoogleCalendars(promptIfNecessary: true);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
final friendlyMessage = _friendlyGoogleError(
|
||||
'Google bejelentkez\u00e9s nem siker\u00fclt',
|
||||
error,
|
||||
);
|
||||
final configError = _jarvisConfigError();
|
||||
if (configError != null) {
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = friendlyMessage;
|
||||
statusMessage = configError;
|
||||
});
|
||||
await _showGoogleErrorDialog(friendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendlyGoogleError(String prefix, Object error) {
|
||||
final raw = error.toString();
|
||||
if (raw.contains('serverClientId must be provided') ||
|
||||
raw.contains('CredentialManager requires a serverClientId')) {
|
||||
return '$prefix: hi\u00e1nyzik az app Google konfigur\u00e1ci\u00f3ja. '
|
||||
'A bel\u00e9p\u00e9si ablakos m\u0171k\u00f6d\u00e9shez tedd be az android/app/google-services.json f\u00e1jlt, '
|
||||
'amely tartalmaz Web application OAuth client ID-t, majd buildeld \u00fajra az APK-t.';
|
||||
}
|
||||
if (raw.contains('Account reauth failed')) {
|
||||
return '$prefix: a Google fi\u00f3k \u00fajrahiteles\u00edt\u00e9se nem siker\u00fclt. '
|
||||
'Pr\u00f3b\u00e1ld meg a tableten a Google fi\u00f3kot friss\u00edteni/bejelentkeztetni a Be\u00e1ll\u00edt\u00e1sokban, '
|
||||
'vagy t\u00f6r\u00f6ld az app Google hozz\u00e1f\u00e9r\u00e9s\u00e9t, majd jelentkezz be \u00fajra.';
|
||||
}
|
||||
return '$prefix: $raw';
|
||||
}
|
||||
|
||||
Future<void> _showGoogleErrorDialog(String message) async {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Google bel\u00e9p\u00e9s nem indult el'),
|
||||
content: Text(
|
||||
'$message\n\n'
|
||||
'A v\u00e1rt m\u0171k\u00f6d\u00e9s az, hogy a Google saj\u00e1t bel\u00e9p\u00e9si/enged\u00e9lyez\u00e9si ablaka ny\u00edlik meg. '
|
||||
'Ehhez az appnak el\u0151re regisztr\u00e1lt Google konfigur\u00e1ci\u00f3 kell, \u00e9s az eszk\u00f6z\u00f6n Google Play Services / Google fi\u00f3k is sz\u00fcks\u00e9ges.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Rendben'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _ensureGoogleInitialized() async {
|
||||
final clientId = runtimeServerClientId;
|
||||
if (clientId == null || clientId.isEmpty) {
|
||||
await googleSignIn.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
await googleSignIn.initialize(
|
||||
serverClientId: clientId,
|
||||
);
|
||||
try {
|
||||
final response = await http
|
||||
.get(_jarvisUri('/api/google/oauth/start'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('JARVIS OAuth ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
final payload = jsonDecode(response.body);
|
||||
if (payload is! Map<String, dynamic>) throw Exception('Hib\u00e1s JARVIS v\u00e1lasz.');
|
||||
final url = payload['authorizationUrl'] as String?;
|
||||
if (url == null || url.isEmpty) throw Exception('Hi\u00e1nyzik az OAuth URL.');
|
||||
final launched = await launchUrl(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!launched) throw Exception('Nem siker\u00fclt megnyitni a b\u00f6ng\u00e9sz\u0151t.');
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage =
|
||||
'A Google enged\u00e9lyez\u00e9s b\u00f6ng\u00e9sz\u0151ben megny\u00edlt. Ha k\u00e9sz, t\u00e9rj vissza ide \u00e9s friss\u00edtsd a napt\u00e1rlist\u00e1t.';
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
loadingCalendars = false;
|
||||
statusMessage = 'Google \u00f6sszek\u00f6t\u00e9s nem indult el: $error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadGoogleCalendars({bool promptIfNecessary = true}) async {
|
||||
Future<void> _loadGoogleCalendars() async {
|
||||
setState(() {
|
||||
loadingCalendars = true;
|
||||
statusMessage = 'Google napt\u00e1rak lek\u00e9r\u00e9se...';
|
||||
});
|
||||
|
||||
try {
|
||||
final headers = await _googleHeaders(
|
||||
promptIfNecessary: promptIfNecessary,
|
||||
);
|
||||
final uri = Uri.https(
|
||||
'www.googleapis.com',
|
||||
'/calendar/v3/users/me/calendarList',
|
||||
{'minAccessRole': 'reader', 'showHidden': 'true'},
|
||||
);
|
||||
final response = await http
|
||||
.get(uri, headers: headers)
|
||||
.get(_jarvisUri('/api/google/calendars'), headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'CalendarList API ${response.statusCode}: ${response.body}',
|
||||
'JARVIS CalendarList ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,16 +202,15 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
return a.name.compareTo(b.name);
|
||||
});
|
||||
|
||||
final account = signedInAccount;
|
||||
final current = connection;
|
||||
if (account != null) {
|
||||
if (current == null) {
|
||||
widget.onChanged([
|
||||
GoogleCalendarConnection(
|
||||
id: current?.id ?? uniqueId(),
|
||||
accountEmail: account.email,
|
||||
enabledCalendarIds: current?.enabledCalendarIds ?? const [],
|
||||
calendarPersonIds: current?.calendarPersonIds ?? const {},
|
||||
lastSync: current?.lastSync,
|
||||
id: uniqueId(),
|
||||
accountEmail: 'Google Calendar',
|
||||
enabledCalendarIds: const [],
|
||||
calendarPersonIds: const {},
|
||||
lastSync: null,
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -312,9 +250,8 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
|
||||
try {
|
||||
if (googleCalendars.isEmpty) {
|
||||
await _loadGoogleCalendars(promptIfNecessary: true);
|
||||
await _loadGoogleCalendars();
|
||||
}
|
||||
final headers = await _googleHeaders(promptIfNecessary: true);
|
||||
final selectedCalendars = googleCalendars
|
||||
.where((calendar) => current.enabledCalendarIds.contains(calendar.id))
|
||||
.toList();
|
||||
@@ -329,19 +266,16 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
final end = start.add(const Duration(days: 180));
|
||||
|
||||
for (final calendar in selectedCalendars) {
|
||||
final uri = Uri.https(
|
||||
'www.googleapis.com',
|
||||
'/calendar/v3/calendars/${Uri.encodeComponent(calendar.id)}/events',
|
||||
final uri = _jarvisUri(
|
||||
'/api/google/events',
|
||||
{
|
||||
'singleEvents': 'true',
|
||||
'orderBy': 'startTime',
|
||||
'calendarId': calendar.id,
|
||||
'timeMin': start.toUtc().toIso8601String(),
|
||||
'timeMax': end.toUtc().toIso8601String(),
|
||||
'maxResults': '2500',
|
||||
},
|
||||
);
|
||||
final response = await http
|
||||
.get(uri, headers: headers)
|
||||
.get(uri, headers: _jarvisHeaders)
|
||||
.timeout(const Duration(seconds: 20));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('${calendar.name}: Events API ${response.statusCode}');
|
||||
@@ -396,13 +330,13 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
.length;
|
||||
|
||||
return PageFrame(
|
||||
eyebrow: 'Google Calendar API szinkron',
|
||||
eyebrow: 'JARVIS Google Calendar szinkron',
|
||||
title: 'Google napt\u00e1rak',
|
||||
action: FilledButton.icon(
|
||||
onPressed: loadingCalendars ? null : _signInGoogle,
|
||||
icon: const Icon(Icons.login),
|
||||
onPressed: loadingCalendars ? null : _startGoogleConnection,
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text(
|
||||
hasConnection ? 'Fi\u00f3k v\u00e1lt\u00e1sa' : 'Google bel\u00e9p\u00e9s',
|
||||
hasConnection ? 'Google \u00f6sszek\u00f6t\u00e9s \u00fajranyit\u00e1sa' : 'Google \u00f6sszek\u00f6t\u00e9s',
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
@@ -491,24 +425,23 @@ class _AccountsPageState extends State<AccountsPage> {
|
||||
OutlinedButton.icon(
|
||||
onPressed: loadingCalendars
|
||||
? null
|
||||
: () => _loadGoogleCalendars(promptIfNecessary: true),
|
||||
: _loadGoogleConnectionStatus,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Google napt\u00e1rlista friss\u00edt\u00e9se'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await googleSignIn.disconnect();
|
||||
widget.onChanged([]);
|
||||
widget.onSyncedEventsChanged([]);
|
||||
setState(() {
|
||||
signedInAccount = null;
|
||||
googleCalendars = [];
|
||||
statusMessage = 'Google kapcsolat lev\u00e1lasztva.';
|
||||
statusMessage =
|
||||
'Helyi Google kapcsolat t\u00f6r\u00f6lve. A JARVIS token szerveren marad; \u00faj \u00f6sszek\u00f6t\u00e9ssel fel\u00fcl\u00edrhat\u00f3.';
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.link_off),
|
||||
label: const Text('Google kapcsolat lev\u00e1laszt\u00e1sa'),
|
||||
label: const Text('Helyi Google cache t\u00f6rl\u00e9se'),
|
||||
),
|
||||
],
|
||||
if (statusMessage != null) ...[
|
||||
|
||||
+1
-1
@@ -7,11 +7,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geocoding/geocoding.dart' as geocoding;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:timezone/data/latest.dart' as timezone_data;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
part 'data.dart';
|
||||
part 'app_shell.dart';
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -7,14 +7,14 @@ import Foundation
|
||||
|
||||
import geocoding_darwin
|
||||
import geolocator_apple
|
||||
import google_sign_in_ios
|
||||
import mobile_scanner
|
||||
import package_info_plus
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
GeocodingDarwinPlugin.register(with: registry.registrar(forPlugin: "GeocodingDarwinPlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
+64
-48
@@ -304,54 +304,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_sign_in:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in
|
||||
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
google_sign_in_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: "57782125965ca87b03d42a147d40b046f1fd6a09141a58913e1b86671a5ef22e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.16"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_platform_interface
|
||||
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -685,6 +637,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.32"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ dependencies:
|
||||
geolocator: ^14.0.3
|
||||
geocoding: ^5.0.0
|
||||
mobile_scanner: ^7.4.0
|
||||
google_sign_in: ^7.2.0
|
||||
url_launcher: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
geolocator_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user