Compare commits
5 Commits
fade61ac21
...
sccsbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9d56b07c | ||
|
|
e52e21d295 | ||
| ec5af19d30 | |||
|
|
bd4350c17f | ||
|
|
fe4c81c54f |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,10 @@
|
|||||||
*.key
|
*.key
|
||||||
*.pfx
|
*.pfx
|
||||||
|
|
||||||
|
# Android release keystore
|
||||||
|
health_app/android/**/*.jks
|
||||||
|
health_app/android/key.properties
|
||||||
|
|
||||||
# .NET build outputs
|
# .NET build outputs
|
||||||
backend/**/bin/
|
backend/**/bin/
|
||||||
backend/**/obj/
|
backend/**/obj/
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ namespace Health.Application.Auth;
|
|||||||
|
|
||||||
public sealed record AuthResult(int Code, object? Data, string? Message = null);
|
public sealed record AuthResult(int Code, object? Data, string? Message = null);
|
||||||
public sealed record RegisterCommand(string Phone, string SmsCode, string Name, Guid DoctorId);
|
public sealed record RegisterCommand(string Phone, string SmsCode, string Name, Guid DoctorId);
|
||||||
|
public sealed record AppleLoginCommand(string IdentityToken, string? AuthorizationCode, string? Name);
|
||||||
|
|
||||||
public interface IAuthService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct);
|
Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct);
|
||||||
Task<AuthResult> RegisterAsync(RegisterCommand command, CancellationToken ct);
|
Task<AuthResult> RegisterAsync(RegisterCommand command, CancellationToken ct);
|
||||||
Task<AuthResult> LoginAsync(string phone, string smsCode, CancellationToken ct);
|
Task<AuthResult> LoginAsync(string phone, string smsCode, CancellationToken ct);
|
||||||
|
Task<AuthResult> AppleLoginAsync(AppleLoginCommand command, CancellationToken ct);
|
||||||
Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct);
|
Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct);
|
||||||
Task LogoutAsync(string refreshToken, CancellationToken ct);
|
Task LogoutAsync(string refreshToken, CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace Health.Application.Users;
|
|||||||
|
|
||||||
public sealed record UserProfileDto(
|
public sealed record UserProfileDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Phone,
|
string? Phone, // Apple 用户无手机号
|
||||||
string Role,
|
string Role,
|
||||||
string? Name,
|
string? Name,
|
||||||
string? Gender,
|
string? Gender,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ namespace Health.Domain.Entities;
|
|||||||
public sealed class User
|
public sealed class User
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Phone { get; set; } = string.Empty;
|
public string? Phone { get; set; } // Apple 用户无手机号
|
||||||
|
public string? AppleUserId { get; set; } // Apple Sign-In 唯一标识
|
||||||
public string Role { get; set; } = "User"; // "User" | "Doctor" | "Admin"
|
public string Role { get; set; } = "User"; // "User" | "Doctor" | "Admin"
|
||||||
public Guid? DoctorId { get; set; } // 患者选择的医生
|
public Guid? DoctorId { get; set; } // 患者选择的医生
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ using Health.Infrastructure.Services;
|
|||||||
|
|
||||||
namespace Health.Infrastructure.Auth;
|
namespace Health.Infrastructure.Auth;
|
||||||
|
|
||||||
public sealed class AuthService(AppDbContext db, JwtProvider jwt, SmsService sms) : IAuthService
|
public sealed class AuthService(
|
||||||
|
AppDbContext db,
|
||||||
|
JwtProvider jwt,
|
||||||
|
SmsService sms,
|
||||||
|
AppleTokenValidator appleValidator // Apple Sign-In JWT 验证
|
||||||
|
) : IAuthService
|
||||||
{
|
{
|
||||||
private const string AdminPhone = "12345678910";
|
private const string AdminPhone = "12345678910";
|
||||||
private const string AdminSmsCode = "000000";
|
private const string AdminSmsCode = "000000";
|
||||||
@@ -11,6 +16,7 @@ public sealed class AuthService(AppDbContext db, JwtProvider jwt, SmsService sms
|
|||||||
private readonly AppDbContext _db = db;
|
private readonly AppDbContext _db = db;
|
||||||
private readonly JwtProvider _jwt = jwt;
|
private readonly JwtProvider _jwt = jwt;
|
||||||
private readonly SmsService _sms = sms;
|
private readonly SmsService _sms = sms;
|
||||||
|
private readonly AppleTokenValidator _appleValidator = appleValidator;
|
||||||
|
|
||||||
public async Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct)
|
public async Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -99,13 +105,64 @@ public sealed class AuthService(AppDbContext db, JwtProvider jwt, SmsService sms
|
|||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AuthResult> AppleLoginAsync(AppleLoginCommand command, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. 验证 Apple identityToken
|
||||||
|
string appleUserId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
appleUserId = await _appleValidator.ValidateAsync(command.IdentityToken, ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error(40005, $"Apple 登录验证失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找已有用户
|
||||||
|
var existingUser = await _db.Users.FirstOrDefaultAsync(x => x.AppleUserId == appleUserId, ct);
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
existingUser.UpdatedAt = DateTime.UtcNow;
|
||||||
|
if (!string.IsNullOrWhiteSpace(command.Name)) existingUser.Name ??= command.Name;
|
||||||
|
var tokens = AddTokens(existingUser.Id, existingUser.Phone, existingUser.Role);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
return new AuthResult(0, new
|
||||||
|
{
|
||||||
|
tokens.accessToken, tokens.refreshToken,
|
||||||
|
user = new { existingUser.Id, existingUser.Phone, existingUser.Role, existingUser.Name, existingUser.Gender, existingUser.AvatarUrl, isNew = false }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 新用户 → 创建账户(无需手机号、无需选医生)
|
||||||
|
var newUser = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Phone = null,
|
||||||
|
AppleUserId = appleUserId,
|
||||||
|
Role = "User",
|
||||||
|
Name = command.Name,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
_db.Users.Add(newUser);
|
||||||
|
_db.NotificationPreferences.Add(new NotificationPreference { Id = Guid.NewGuid(), UserId = newUser.Id });
|
||||||
|
_db.HealthArchives.Add(new HealthArchive { Id = Guid.NewGuid(), UserId = newUser.Id });
|
||||||
|
var newTokens = AddTokens(newUser.Id, null, newUser.Role);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
return new AuthResult(0, new
|
||||||
|
{
|
||||||
|
newTokens.accessToken, newTokens.refreshToken,
|
||||||
|
user = new { newUser.Id, Phone = (string?)null, newUser.Role, newUser.Name, isNew = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Task<VerificationCode?> TakeCodeAsync(string phone, string code, CancellationToken ct) =>
|
private Task<VerificationCode?> TakeCodeAsync(string phone, string code, CancellationToken ct) =>
|
||||||
_db.VerificationCodes.Where(x => x.Phone == phone && x.Code == code && x.ExpiresAt > DateTime.UtcNow && !x.IsUsed)
|
_db.VerificationCodes.Where(x => x.Phone == phone && x.Code == code && x.ExpiresAt > DateTime.UtcNow && !x.IsUsed)
|
||||||
.OrderByDescending(x => x.CreatedAt).FirstOrDefaultAsync(ct);
|
.OrderByDescending(x => x.CreatedAt).FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
private (string accessToken, string refreshToken) AddTokens(Guid userId, string phone, string role)
|
private (string accessToken, string refreshToken) AddTokens(Guid userId, string? phone, string role)
|
||||||
{
|
{
|
||||||
var access = _jwt.GenerateAccessToken(userId, phone, role);
|
var access = _jwt.GenerateAccessToken(userId, phone ?? "", role);
|
||||||
var refresh = _jwt.GenerateRefreshToken();
|
var refresh = _jwt.GenerateRefreshToken();
|
||||||
_db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = refresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
_db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = refresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||||||
return (access, refresh);
|
return (access, refresh);
|
||||||
|
|||||||
1466
backend/src/Health.Infrastructure/Data/Migrations/20260710052811_AddAppleSignIn.Designer.cs
generated
Normal file
1466
backend/src/Health.Infrastructure/Data/Migrations/20260710052811_AddAppleSignIn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Health.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAppleSignIn : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Phone",
|
||||||
|
table: "Users",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "AppleUserId",
|
||||||
|
table: "Users",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_AppleUserId",
|
||||||
|
table: "Users",
|
||||||
|
column: "AppleUserId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Users_AppleUserId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AppleUserId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Phone",
|
||||||
|
table: "Users",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -803,6 +803,9 @@ namespace Health.Infrastructure.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AppleUserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("AvatarUrl")
|
b.Property<string>("AvatarUrl")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
@@ -822,7 +825,6 @@ namespace Health.Infrastructure.Data.Migrations
|
|||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Phone")
|
b.Property<string>("Phone")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Role")
|
b.Property<string>("Role")
|
||||||
@@ -837,6 +839,9 @@ namespace Health.Infrastructure.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AppleUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.HasIndex("DoctorId");
|
b.HasIndex("DoctorId");
|
||||||
|
|
||||||
b.HasIndex("Phone")
|
b.HasIndex("Phone")
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
|
|||||||
builder.Entity<User>(e =>
|
builder.Entity<User>(e =>
|
||||||
{
|
{
|
||||||
e.HasIndex(u => u.Phone).IsUnique();
|
e.HasIndex(u => u.Phone).IsUnique();
|
||||||
|
e.HasIndex(u => u.AppleUserId).IsUnique(); // Apple Sign-In 唯一标识
|
||||||
e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
|
e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
|
||||||
e.HasOne(u => u.Doctor).WithMany().HasForeignKey(u => u.DoctorId).IsRequired(false).OnDelete(DeleteBehavior.SetNull);
|
e.HasOne(u => u.Doctor).WithMany().HasForeignKey(u => u.DoctorId).IsRequired(false).OnDelete(DeleteBehavior.SetNull);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Health.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apple IdentityToken 验证:下载 Apple 公钥、验签、检查声明
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AppleTokenValidator
|
||||||
|
{
|
||||||
|
private const string AppleKeysUrl = "https://appleid.apple.com/auth/keys";
|
||||||
|
private const string AppleIssuer = "https://appleid.apple.com";
|
||||||
|
private static readonly HttpClient _http = new();
|
||||||
|
private static List<JsonWebKey>? _cachedKeys;
|
||||||
|
private static DateTime _keysExpiry = DateTime.MinValue;
|
||||||
|
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
|
||||||
|
private readonly string _clientId;
|
||||||
|
|
||||||
|
public AppleTokenValidator(IConfiguration config)
|
||||||
|
{
|
||||||
|
_clientId = config["APPLE_CLIENT_ID"] ?? "com.datalumina.YYA";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Apple identityToken,返回 sub(用户唯一标识)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> ValidateAsync(string identityToken, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
// 先解码获取 kid
|
||||||
|
var rawToken = handler.ReadJwtToken(identityToken);
|
||||||
|
var kid = rawToken.Header.Kid;
|
||||||
|
|
||||||
|
var keys = await GetKeysAsync(ct);
|
||||||
|
var key = keys.FirstOrDefault(k => k.KeyId == kid)
|
||||||
|
?? throw new SecurityTokenException($"No matching key for kid: {kid}");
|
||||||
|
|
||||||
|
var validationParams = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = AppleIssuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudiences = [_clientId, "com.datalumina.YYA.signin"],
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = key,
|
||||||
|
ClockSkew = TimeSpan.FromMinutes(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler.ValidateToken(identityToken, validationParams, out _);
|
||||||
|
// 从原始 JWT 取 sub(避免 .NET 的 ClaimType 映射把 "sub" 转成 NameIdentifier)
|
||||||
|
var sub = rawToken.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(sub))
|
||||||
|
throw new SecurityTokenException("Missing 'sub' claim in Apple identityToken");
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<List<JsonWebKey>> GetKeysAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_cachedKeys != null && DateTime.UtcNow < _keysExpiry) return _cachedKeys;
|
||||||
|
|
||||||
|
await _lock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_cachedKeys != null && DateTime.UtcNow < _keysExpiry) return _cachedKeys;
|
||||||
|
|
||||||
|
var response = await _http.GetStringAsync(AppleKeysUrl, ct);
|
||||||
|
var jwks = new JsonWebKeySet(response);
|
||||||
|
_cachedKeys = jwks.Keys.ToList();
|
||||||
|
_keysExpiry = DateTime.UtcNow.AddHours(6); // 缓存 6 小时
|
||||||
|
return _cachedKeys;
|
||||||
|
}
|
||||||
|
finally { _lock.Release(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public static class AuthEndpoints
|
|||||||
ToResult(await auth.RegisterAsync(new RegisterCommand(request.Phone, request.SmsCode, request.Name, request.DoctorId), ct)));
|
ToResult(await auth.RegisterAsync(new RegisterCommand(request.Phone, request.SmsCode, request.Name, request.DoctorId), ct)));
|
||||||
app.MapPost("/api/auth/login", async (LoginRequest request, IAuthService auth, CancellationToken ct) =>
|
app.MapPost("/api/auth/login", async (LoginRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
ToResult(await auth.LoginAsync(request.Phone, request.SmsCode, ct)));
|
ToResult(await auth.LoginAsync(request.Phone, request.SmsCode, ct)));
|
||||||
|
app.MapPost("/api/auth/apple-login", async (AppleLoginRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.AppleLoginAsync(new AppleLoginCommand(request.IdentityToken, request.AuthorizationCode, request.Name), ct)));
|
||||||
app.MapPost("/api/auth/refresh", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
app.MapPost("/api/auth/refresh", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
ToResult(await auth.RefreshAsync(request.RefreshToken, ct)));
|
ToResult(await auth.RefreshAsync(request.RefreshToken, ct)));
|
||||||
app.MapPost("/api/auth/logout", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
app.MapPost("/api/auth/logout", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
@@ -28,4 +30,5 @@ public static class AuthEndpoints
|
|||||||
public sealed record SendSmsRequest(string Phone);
|
public sealed record SendSmsRequest(string Phone);
|
||||||
public sealed record RegisterRequest(string Phone, string SmsCode, string Name, Guid DoctorId);
|
public sealed record RegisterRequest(string Phone, string SmsCode, string Name, Guid DoctorId);
|
||||||
public sealed record LoginRequest(string Phone, string SmsCode);
|
public sealed record LoginRequest(string Phone, string SmsCode);
|
||||||
|
public sealed record AppleLoginRequest(string IdentityToken, string? AuthorizationCode, string? Name);
|
||||||
public sealed record RefreshRequest(string RefreshToken);
|
public sealed record RefreshRequest(string RefreshToken);
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ builder.Services.AddAuthorization();
|
|||||||
// ---- 业务服务 ----
|
// ---- 业务服务 ----
|
||||||
builder.Services.AddSingleton<JwtProvider>();
|
builder.Services.AddSingleton<JwtProvider>();
|
||||||
builder.Services.AddSingleton<SmsService>();
|
builder.Services.AddSingleton<SmsService>();
|
||||||
|
builder.Services.AddSingleton<AppleTokenValidator>(); // Apple Sign-In JWT 验证
|
||||||
builder.Services.AddSingleton<PromptManager>();
|
builder.Services.AddSingleton<PromptManager>();
|
||||||
builder.Services.AddScoped<IAiWriteConfirmationStore, EfAiWriteConfirmationStore>();
|
builder.Services.AddScoped<IAiWriteConfirmationStore, EfAiWriteConfirmationStore>();
|
||||||
builder.Services.AddScoped<IAiToolExecutionService, AiToolExecutionService>();
|
builder.Services.AddScoped<IAiToolExecutionService, AiToolExecutionService>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Warning",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
688
docs/superpowers/plans/2026-07-10-apple-sign-in.md
Normal file
688
docs/superpowers/plans/2026-07-10-apple-sign-in.md
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
# Apple Sign-In 集成实施方案
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 为健康管家 App 集成 Sign in with Apple,让 iOS 用户可以通过 Apple ID 一键登录/注册。
|
||||||
|
|
||||||
|
**Architecture:** 后端新增 `POST /api/auth/apple-login` 端点,验证 Apple 返回的 identityToken(JWT),查找或创建 User 记录(无需手机号),返回 JWT token。前端添加 `sign_in_with_apple` Flutter 包,在登录页放置 Apple 登录按钮。
|
||||||
|
|
||||||
|
**Tech Stack:** Flutter 3.44 + Dart 3.12 | C# .NET 10 + EF Core + PostgreSQL | sign_in_with_apple 包 | Apple RSA 公钥验证
|
||||||
|
|
||||||
|
**设计决策:**
|
||||||
|
- User.Phone 改为可空(Apple 用户无手机号),PostgreSQL 对多个 NULL 值不违反唯一索引
|
||||||
|
- 新增 `AppleUserId` 字段(可空字符串 + 唯一索引)标识 Apple 用户
|
||||||
|
- Apple 登录首次无需绑定医生(doctorId 可为空),用户可在个人中心后续设置
|
||||||
|
- Apple identityToken 验证:缓存 Apple 公钥,本地验签,验证 audience/issuer/有效期
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 后端 — User 实体 + DB 迁移
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/src/Health.Domain/Entities/user.cs`
|
||||||
|
- Modify: `backend/src/Health.Infrastructure/Data/app_db_context.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: User 实体添加 AppleUserId 字段,Phone 改为可空**
|
||||||
|
|
||||||
|
`backend/src/Health.Domain/Entities/user.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace Health.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? Phone { get; set; } // 改为可空,Apple 用户无手机号
|
||||||
|
public string? AppleUserId { get; set; } // Apple 唯一标识
|
||||||
|
public string Role { get; set; } = "User";
|
||||||
|
public Guid? DoctorId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
// ... 其余字段不变
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: AppDbContext 添加 AppleUserId 配置**
|
||||||
|
|
||||||
|
`backend/src/Health.Infrastructure/Data/app_db_context.cs` 的 User 配置段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Entity<User>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(u => u.Phone).IsUnique();
|
||||||
|
e.HasIndex(u => u.AppleUserId).IsUnique(); // 新增
|
||||||
|
e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/src/Health.WebApi && dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 后端 — Apple JWT 验证服务
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend/src/Health.Infrastructure/Services/apple_token_validator.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 AppleTokenValidator**
|
||||||
|
|
||||||
|
`backend/src/Health.Infrastructure/Services/apple_token_validator.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Health.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apple IdentityToken 验证:下载公钥、验签、检查声明
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AppleTokenValidator
|
||||||
|
{
|
||||||
|
private const string AppleKeysUrl = "https://appleid.apple.com/auth/keys";
|
||||||
|
private const string AppleIssuer = "https://appleid.apple.com";
|
||||||
|
private static readonly HttpClient _http = new();
|
||||||
|
private static List<JsonWebKey>? _cachedKeys;
|
||||||
|
private static DateTime _keysExpiry = DateTime.MinValue;
|
||||||
|
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
|
|
||||||
|
private readonly string _clientId;
|
||||||
|
|
||||||
|
public AppleTokenValidator(IConfiguration config)
|
||||||
|
{
|
||||||
|
_clientId = config["APPLE_CLIENT_ID"] ?? "com.datalumina.YYA";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Apple identityToken,返回 sub(用户唯一标识)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> ValidateAsync(string identityToken, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
// 先解码 header 获取 kid
|
||||||
|
var rawToken = handler.ReadJwtToken(identityToken);
|
||||||
|
var kid = rawToken.Header.Kid;
|
||||||
|
|
||||||
|
var keys = await GetKeysAsync(ct);
|
||||||
|
var key = keys.FirstOrDefault(k => k.KeyId == kid)
|
||||||
|
?? throw new SecurityTokenException($"No matching key for kid: {kid}");
|
||||||
|
|
||||||
|
var validationParams = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = AppleIssuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudiences = [_clientId, "com.datalumina.YYA.signin"],
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = key,
|
||||||
|
ClockSkew = TimeSpan.FromMinutes(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
var principal = handler.ValidateToken(identityToken, validationParams, out _);
|
||||||
|
var sub = principal.FindFirst("sub")?.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(sub))
|
||||||
|
throw new SecurityTokenException("Missing 'sub' claim in Apple identityToken");
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<List<JsonWebKey>> GetKeysAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_cachedKeys != null && DateTime.UtcNow < _keysExpiry) return _cachedKeys;
|
||||||
|
|
||||||
|
await _lock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_cachedKeys != null && DateTime.UtcNow < _keysExpiry) return _cachedKeys;
|
||||||
|
|
||||||
|
var response = await _http.GetStringAsync(AppleKeysUrl, ct);
|
||||||
|
var jwks = new JsonWebKeySet(response);
|
||||||
|
_cachedKeys = jwks.Keys.ToList();
|
||||||
|
_keysExpiry = DateTime.UtcNow.AddHours(6); // 缓存 6 小时
|
||||||
|
return _cachedKeys;
|
||||||
|
}
|
||||||
|
finally { _lock.Release(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 注册服务到 DI**
|
||||||
|
|
||||||
|
在 `backend/src/Health.WebApi/Program.cs` 的 services 注册区添加:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddSingleton<AppleTokenValidator>();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/src/Health.WebApi && dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: 后端 — AuthService 添加 Apple 登录
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/src/Health.Application/Auth/AuthContracts.cs`
|
||||||
|
- Modify: `backend/src/Health.Infrastructure/Auth/AuthService.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 AppleLoginCommand DTO 和接口方法**
|
||||||
|
|
||||||
|
`backend/src/Health.Application/Auth/AuthContracts.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace Health.Application.Auth;
|
||||||
|
|
||||||
|
public sealed record AuthResult(int Code, object? Data, string? Message = null);
|
||||||
|
public sealed record RegisterCommand(string Phone, string SmsCode, string Name, Guid DoctorId);
|
||||||
|
public sealed record AppleLoginCommand(string IdentityToken, string? AuthorizationCode, string? Name); // 新增
|
||||||
|
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct);
|
||||||
|
Task<AuthResult> RegisterAsync(RegisterCommand command, CancellationToken ct);
|
||||||
|
Task<AuthResult> LoginAsync(string phone, string smsCode, CancellationToken ct);
|
||||||
|
Task<AuthResult> AppleLoginAsync(AppleLoginCommand command, CancellationToken ct); // 新增
|
||||||
|
Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct);
|
||||||
|
Task LogoutAsync(string refreshToken, CancellationToken ct);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: AuthService 实现 AppleLoginAsync**
|
||||||
|
|
||||||
|
`backend/src/Health.Infrastructure/Auth/AuthService.cs`:
|
||||||
|
|
||||||
|
在类的顶部依赖注入区添加 `AppleTokenValidator`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class AuthService(
|
||||||
|
AppDbContext db,
|
||||||
|
JwtProvider jwt,
|
||||||
|
SmsService sms,
|
||||||
|
AppleTokenValidator appleValidator // 新增
|
||||||
|
) : IAuthService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db = db;
|
||||||
|
private readonly JwtProvider _jwt = jwt;
|
||||||
|
private readonly SmsService _sms = sms;
|
||||||
|
private readonly AppleTokenValidator _appleValidator = appleValidator;
|
||||||
|
// ... 现有代码不变
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `LogoutAsync` 方法之后添加新方法,并修改 `AddTokens` 的 phone 参数类型为 `string?`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<AuthResult> AppleLoginAsync(AppleLoginCommand command, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. 验证 Apple identityToken
|
||||||
|
string appleUserId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
appleUserId = await _appleValidator.ValidateAsync(command.IdentityToken, ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error(40005, $"Apple 登录验证失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找已有用户
|
||||||
|
var existingUser = await _db.Users.FirstOrDefaultAsync(x => x.AppleUserId == appleUserId, ct);
|
||||||
|
if (existingUser != null)
|
||||||
|
{
|
||||||
|
// 已有用户 → 直接登录
|
||||||
|
existingUser.UpdatedAt = DateTime.UtcNow;
|
||||||
|
if (!string.IsNullOrWhiteSpace(command.Name)) existingUser.Name ??= command.Name;
|
||||||
|
var tokens = AddTokens(existingUser.Id, existingUser.Phone, existingUser.Role);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
return new AuthResult(0, new
|
||||||
|
{
|
||||||
|
tokens.accessToken, tokens.refreshToken,
|
||||||
|
user = new { existingUser.Id, existingUser.Phone, existingUser.Role, existingUser.Name, existingUser.Gender, existingUser.AvatarUrl, isNew = false }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 新用户 → 创建账户(无需手机号、无需选医生)
|
||||||
|
var newUser = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Phone = null, // Apple 用户未绑定手机
|
||||||
|
AppleUserId = appleUserId,
|
||||||
|
Role = "User",
|
||||||
|
Name = command.Name,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
_db.Users.Add(newUser);
|
||||||
|
_db.NotificationPreferences.Add(new NotificationPreference { Id = Guid.NewGuid(), UserId = newUser.Id });
|
||||||
|
_db.HealthArchives.Add(new HealthArchive { Id = Guid.NewGuid(), UserId = newUser.Id });
|
||||||
|
var newTokens = AddTokens(newUser.Id, null, newUser.Role);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
return new AuthResult(0, new
|
||||||
|
{
|
||||||
|
newTokens.accessToken, newTokens.refreshToken,
|
||||||
|
user = new { newUser.Id, Phone = (string?)null, newUser.Role, newUser.Name, isNew = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同时修改 `AddTokens` 方法签名,phone 改为可空:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private (string accessToken, string refreshToken) AddTokens(Guid userId, string? phone, string role)
|
||||||
|
{
|
||||||
|
var access = _jwt.GenerateAccessToken(userId, phone ?? "", role);
|
||||||
|
var refresh = _jwt.GenerateRefreshToken();
|
||||||
|
_db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = refresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||||||
|
return (access, refresh);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: `AddTokens` 的 phone 参数改为 `string?`,`GenerateAccessToken` 接收到空字符串(Apple 用户无需手机号)。`RefreshAsync` 和 `RegisterAsync` 中现有的 `AddTokens` 调用无需修改(传入 `string?` 的 `user.Phone` 自动匹配)。Admin 的 `AddTokens(AdminId, AdminPhone, "Admin")`(`AdminPhone` 是 `string` 常量)也无需修改。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/src/Health.WebApi && dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 后端 — 添加 Apple 登录端点
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/src/Health.WebApi/Endpoints/auth_endpoints.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 AppleLoginRequest 和端点**
|
||||||
|
|
||||||
|
`backend/src/Health.WebApi/Endpoints/auth_endpoints.cs`:
|
||||||
|
|
||||||
|
在现有端点中添加:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 在 MapAuthEndpoints 方法内、logout 端点之后添加:
|
||||||
|
app.MapPost("/api/auth/apple-login", async (AppleLoginRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.AppleLoginAsync(new AppleLoginCommand(request.IdentityToken, request.AuthorizationCode, request.Name), ct)));
|
||||||
|
```
|
||||||
|
|
||||||
|
在文件末尾添加 DTO:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record AppleLoginRequest(string IdentityToken, string? AuthorizationCode, string? Name);
|
||||||
|
```
|
||||||
|
|
||||||
|
文件最终结果为:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Health.Application.Auth;
|
||||||
|
|
||||||
|
namespace Health.WebApi.Endpoints;
|
||||||
|
|
||||||
|
public static class AuthEndpoints
|
||||||
|
{
|
||||||
|
public static void MapAuthEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.MapPost("/api/auth/send-sms", async (SendSmsRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.SendCodeAsync(request.Phone, app.Environment.IsDevelopment(), ct)));
|
||||||
|
app.MapPost("/api/auth/register", async (RegisterRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.RegisterAsync(new RegisterCommand(request.Phone, request.SmsCode, request.Name, request.DoctorId), ct)));
|
||||||
|
app.MapPost("/api/auth/login", async (LoginRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.LoginAsync(request.Phone, request.SmsCode, ct)));
|
||||||
|
app.MapPost("/api/auth/apple-login", async (AppleLoginRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.AppleLoginAsync(new AppleLoginCommand(request.IdentityToken, request.AuthorizationCode, request.Name), ct)));
|
||||||
|
app.MapPost("/api/auth/refresh", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
ToResult(await auth.RefreshAsync(request.RefreshToken, ct)));
|
||||||
|
app.MapPost("/api/auth/logout", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
await auth.LogoutAsync(request.RefreshToken, ct);
|
||||||
|
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult ToResult(AuthResult result) =>
|
||||||
|
Results.Ok(new { code = result.Code, data = result.Data, message = result.Message });
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SendSmsRequest(string Phone);
|
||||||
|
public sealed record RegisterRequest(string Phone, string SmsCode, string Name, Guid DoctorId);
|
||||||
|
public sealed record LoginRequest(string Phone, string SmsCode);
|
||||||
|
public sealed record AppleLoginRequest(string IdentityToken, string? AuthorizationCode, string? Name);
|
||||||
|
public sealed record RefreshRequest(string RefreshToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证编译**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/src/Health.WebApi && dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 后端 — 重启并测试端点连通性
|
||||||
|
|
||||||
|
- [ ] **Step 1: 重启后端服务**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/src/Health.WebApi && dotnet run &
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 测试无 token 调用(预期返回 40005,token 无效)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -w "\nHTTP %{http_code}" -X POST "http://localhost:5000/api/auth/apple-login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"identityToken":"invalid","name":"test"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: HTTP 200, code=40005, message 包含"验证失败"
|
||||||
|
|
||||||
|
- [ ] **Step 3: 测试真实场景**
|
||||||
|
|
||||||
|
此时无法用 curl 直接测(需要真实的 Apple identityToken)。稍后在 Task 8 中从 App 端联合验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端 — 添加 sign_in_with_apple 依赖
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `health_app/pubspec.yaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加依赖**
|
||||||
|
|
||||||
|
`health_app/pubspec.yaml`,在 dependencies 区添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Apple 登录
|
||||||
|
sign_in_with_apple: ^6.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 安装依赖**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd health_app && flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: 前端 — AuthProvider 添加 Apple 登录方法
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `health_app/lib/providers/auth_provider.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 appleLogin 方法**
|
||||||
|
|
||||||
|
`health_app/lib/providers/auth_provider.dart`,在 `login()` 方法之后、`logout()` 方法之前添加:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
/// Apple 登录
|
||||||
|
Future<String?> appleLogin(String identityToken, String? authorizationCode, String? name) async {
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiClientProvider);
|
||||||
|
final response = await api.post(
|
||||||
|
'/api/auth/apple-login',
|
||||||
|
data: {
|
||||||
|
'identityToken': identityToken,
|
||||||
|
if (authorizationCode != null) 'authorizationCode': authorizationCode,
|
||||||
|
if (name != null && name.isNotEmpty) 'name': name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final data = response.data['data'];
|
||||||
|
if (data == null) return response.data['message'] ?? 'Apple 登录失败';
|
||||||
|
|
||||||
|
await api.saveTokens(data['accessToken'], data['refreshToken']);
|
||||||
|
final user = data['user'];
|
||||||
|
state = AuthState(
|
||||||
|
isLoggedIn: true,
|
||||||
|
isLoading: false,
|
||||||
|
user: UserInfo(
|
||||||
|
id: user['id'] ?? '',
|
||||||
|
phone: user['phone'] ?? '',
|
||||||
|
role: user['role'] ?? 'User',
|
||||||
|
name: user['name'],
|
||||||
|
avatarUrl: user['avatarUrl'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Apple 登录失败: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: `UserInfo` 的 `phone` 字段当前是 `required String`。Apple 用户可能无手机号。`user['phone']` 可能是 null。在 login() 方法中 Phone 断言 `phone: user['phone'] ?? ''` 已有兜底,appleLogin 保持一致。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 编译检查**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd health_app && flutter analyze lib/providers/auth_provider.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: 前端 — 登录页添加 Apple 登录按钮
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `health_app/lib/pages/auth/login_page.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 Apple 登录按钮组件和点击逻辑**
|
||||||
|
|
||||||
|
`health_app/lib/pages/auth/login_page.dart`:
|
||||||
|
|
||||||
|
顶部添加 import:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `_LoginPageState` 类中添加 Apple 登录方法(在 `_startCountdown` 方法之后):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> _appleSignIn() async {
|
||||||
|
if (_loading) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
_successMsg = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final credential = await SignInWithApple.getAppleIDCredential(
|
||||||
|
scopes: [
|
||||||
|
AppleIDAuthorizationScopes.fullName,
|
||||||
|
],
|
||||||
|
webAuthenticationOptions: WebAuthenticationOptions(
|
||||||
|
clientId: 'com.datalumina.YYA.signin',
|
||||||
|
redirectUri: Uri.parse('https://erpapi.datalumina.cn/xiaomai/api/auth/apple-login'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final identityToken = credential.identityToken;
|
||||||
|
if (identityToken == null) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = 'Apple 登录未返回身份信息,请重试';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = credential.givenName != null && credential.familyName != null
|
||||||
|
? '${credential.familyName}${credential.givenName}'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final err = await ref.read(authProvider.notifier).appleLogin(
|
||||||
|
identityToken,
|
||||||
|
credential.authorizationCode,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _loading = false);
|
||||||
|
if (err != null) {
|
||||||
|
setState(() => _error = err);
|
||||||
|
} else {
|
||||||
|
_goHome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
if (e is SignInWithAppleAuthorizationException) {
|
||||||
|
_error = 'Apple 登录已取消';
|
||||||
|
} else {
|
||||||
|
_error = 'Apple 登录失败: $e';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `_LoginFormCard` 的 children 中,在 `_PrimaryButton` 和"去注册"链接之间(`_PrimaryButton` 之后,`const SizedBox(height: 16)` 之前)插入 Apple 按钮:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// --- Apple 登录按钮 ---
|
||||||
|
SignInWithAppleButton(
|
||||||
|
onPressed: _appleSignIn,
|
||||||
|
style: SignInWithAppleButtonStyle.whiteOutline,
|
||||||
|
height: 48,
|
||||||
|
cornerRadius: 14,
|
||||||
|
),
|
||||||
|
// --- 结束 ---
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
```
|
||||||
|
|
||||||
|
即在 build 方法中 `_LoginFormCard` child 的末尾(`_PrimaryButton` 之后、`GestureDetector` "去注册" 之前):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ... 上面是 _PrimaryButton 的 const SizedBox(height: 22) 和 _PrimaryButton 本身
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SignInWithAppleButton(
|
||||||
|
onPressed: _appleSignIn,
|
||||||
|
style: SignInWithAppleButtonStyle.whiteOutline,
|
||||||
|
height: 48,
|
||||||
|
cornerRadius: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
_isLogin = !_isLogin;
|
||||||
|
_error = null;
|
||||||
|
_successMsg = null;
|
||||||
|
}),
|
||||||
|
// ... 去注册/去登录 链接
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1 补充**: `_submit()` 方法中也需要加 `_loading` 检查,确保 Apple 按钮点击期间短信登录不响应。现有 `_submit` 已有 `setState(() => _loading = true)`,问题不大。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 编译检查**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd health_app && flutter analyze lib/pages/auth/login_page.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编译 iOS 验证无语法错误**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd health_app && flutter build ios --debug --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: iOS — Xcode 配置 Sign in with Apple
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 Xcode 中启用 Sign in with Apple Capability**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open health_app/ios/Runner.xcworkspace
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Xcode 中操作:
|
||||||
|
1. 选择 Runner target → Signing & Capabilities
|
||||||
|
2. 点击 "+ Capability" → 选择 "Sign in with Apple"
|
||||||
|
3. 确认 .entitlements 文件自动生成了 `com.apple.developer.applesignin` entitlement
|
||||||
|
|
||||||
|
这也会自动在 `project.pbxproj` 中写入相关配置,后续 git diff 可以看到变化。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 验证 .entitlements 文件**
|
||||||
|
|
||||||
|
确认 `health_app/ios/Runner/Runner.entitlements` 包含:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string>Default</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: App Store Connect 配置
|
||||||
|
|
||||||
|
- [ ] **Step 1: 在 App Store Connect 中启用 Sign in with Apple**
|
||||||
|
|
||||||
|
1. 打开 [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
|
2. 进入 App 记录 → App 隐私 → Sign in with Apple
|
||||||
|
3. 确认已为 `com.datalumina.YYA` 这个 App ID 启用了 Sign in with Apple capability
|
||||||
|
|
||||||
|
如果还未创建 App Store Connect 上的 App 记录,需要在 Certificates, Identifiers & Profiles 中为 App ID 手动勾选 "Sign in with Apple" capability。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 验证计划
|
||||||
|
|
||||||
|
**后端单元验证:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 后端编译通过
|
||||||
|
cd backend/src/Health.WebApi && dotnet build
|
||||||
|
|
||||||
|
# 2. 启动后端
|
||||||
|
cd backend/src/Health.WebApi && dotnet run
|
||||||
|
|
||||||
|
# 3. 测试 Apple 登录端点(无 token 场景)
|
||||||
|
curl -s -X POST "http://localhost:5000/api/auth/apple-login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"identityToken":"test","name":"测试"}'
|
||||||
|
# 预期: HTTP 200, body 包含 code=40005
|
||||||
|
|
||||||
|
# 4. 测试现有发送短信端点未被影响
|
||||||
|
curl -s -X POST "http://localhost:5000/api/auth/send-sms" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"phone":"13800138000"}'
|
||||||
|
# 预期: HTTP 200, code=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端验证:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 编译检查
|
||||||
|
cd health_app && flutter analyze
|
||||||
|
|
||||||
|
# 2. iOS 构建通过
|
||||||
|
flutter build ios --debug --no-codesign
|
||||||
|
|
||||||
|
# 3. 在模拟器上运行
|
||||||
|
flutter run -d "iPhone 17 Pro"
|
||||||
|
# 验证:登录页显示 Apple 登录按钮;点击后弹出 Apple ID 授权界面
|
||||||
|
```
|
||||||
|
|
||||||
|
**端到端验证(模拟器):**
|
||||||
|
1. 模拟器上启动 App → 进入登录页
|
||||||
|
2. 点击 Apple 登录按钮 → Apple 授权弹窗出现 → 选择"继续"
|
||||||
|
3. 后端验证 identityToken → 创建新用户或登录已有 → 返回 JWT → App 进入首页
|
||||||
|
4. 二次打开 App → 如果之前 Apple 登录成功 + refresh_token 有效 → 自动恢复登录态
|
||||||
|
|
||||||
|
**DB 验证:**
|
||||||
|
```sql
|
||||||
|
-- 确认 Apple 用户创建成功
|
||||||
|
SELECT id, phone, apple_user_id, name, role FROM "Users" WHERE apple_user_id IS NOT NULL;
|
||||||
|
```
|
||||||
@@ -5,6 +5,13 @@ plugins {
|
|||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载正式签名配置
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
val keystoreProperties = java.util.Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(keystorePropertiesFile.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.datalumina.YYA"
|
namespace = "com.datalumina.YYA"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@@ -20,21 +27,28 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "com.datalumina.YYA"
|
applicationId = "com.datalumina.YYA"
|
||||||
// You can update the following values to match your application needs.
|
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 正式签名配置
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||||
|
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
health_app/appinfo
Normal file
12
health_app/appinfo
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
IOS:
|
||||||
|
SHA1:
|
||||||
|
B1:17:1D:FD:7E:CA:CE:61:65:C2:2D:8B:A0:7C:57:5A:EB:EF:20:5E
|
||||||
|
SHA256:
|
||||||
|
D0:1E:AF:24:40:06:42:1B:FA:86:5E:3D:90:ED:FA:D7:3F:2D:E2:99:52:60:58:F4:85:4F:18:95:1E:B9:CC:96
|
||||||
|
MD5:
|
||||||
|
15:3B:89:ED:6B:C2:1B:6A:F9:6D:E0:71:44:54:CB:0F
|
||||||
|
|
||||||
|
Android:
|
||||||
|
SHA1: 48:48:22:D2:DA:B5:0D:4B:3F:D6:CA:8C:77:B1:36:B5:B5:8F:47:D1
|
||||||
|
SHA256: 0F:10:E7:B5:D0:8A:1E:4B:A6:67:EB:D4:D4:7F:00:C1:08:7E:EB:46:72:51:73:B2:AC:30:DB:69:18:DD:15:A2
|
||||||
|
MD5:D9:75:8F:17:66:17:3F:83:49:1A:82:34:59:9A:08:25
|
||||||
@@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>13.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
43
health_app/ios/Podfile
Normal file
43
health_app/ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Apple Sign-In requires CocoaPods (plugin not yet on SPM)
|
||||||
|
platform :ios, '13.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
22
health_app/ios/Podfile.lock
Normal file
22
health_app/ios/Podfile.lock
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
PODS:
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- sign_in_with_apple (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
sign_in_with_apple:
|
||||||
|
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 261798c42a06ff769f68b1209723cd96e5586217
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@@ -11,9 +11,12 @@
|
|||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
B79095E86F02076D8BE3225F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C5A7F45AA4D485688D46789 /* Pods_Runner.framework */; };
|
||||||
|
D33A34F4E410B032A1D843E3 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D436F0A8387C8ED95A60B0 /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -42,12 +45,18 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
2C5A7F45AA4D485688D46789 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
5D74CE3B9D4E625B6EC3403A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
6331745AE8BC5EE4BCD290B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
65BAE750C7D318CC27513494 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
93E70FBCBD3EC11A08FAD800 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -55,19 +64,46 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
C52233F0D1C275E97BE00A91 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
C6D436F0A8387C8ED95A60B0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
ECE998D63CAE2AC9FF401968 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
5B6028AD445E1E5F91C5C300 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D33A34F4E410B032A1D843E3 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
|
B79095E86F02076D8BE3225F /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
04069A07808CEF12713E8A9C /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECE998D63CAE2AC9FF401968 /* Pods-Runner.debug.xcconfig */,
|
||||||
|
6331745AE8BC5EE4BCD290B9 /* Pods-Runner.release.xcconfig */,
|
||||||
|
65BAE750C7D318CC27513494 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
5D74CE3B9D4E625B6EC3403A /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
93E70FBCBD3EC11A08FAD800 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
C52233F0D1C275E97BE00A91 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -76,9 +112,19 @@
|
|||||||
path = RunnerTests;
|
path = RunnerTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
789F6ED1EF63DAEA6E803F1D /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2C5A7F45AA4D485688D46789 /* Pods_Runner.framework */,
|
||||||
|
C6D436F0A8387C8ED95A60B0 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -94,6 +140,8 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
04069A07808CEF12713E8A9C /* Pods */,
|
||||||
|
789F6ED1EF63DAEA6E803F1D /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -128,8 +176,10 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
C41A5F4922F3BF2ACAF8E553 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
5B6028AD445E1E5F91C5C300 /* Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -145,18 +195,23 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
0A8FFE29BE998F8F679CF732 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
B60F509C791DA959BCD8E827 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@@ -190,6 +245,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@@ -222,6 +280,28 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
0A8FFE29BE998F8F679CF732 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -253,6 +333,45 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
|
B60F509C791DA959BCD8E827 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
C41A5F4922F3BF2ACAF8E553 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -348,8 +467,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
@@ -361,14 +479,16 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = KBM2RZ74VR;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -378,13 +498,14 @@
|
|||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 5D74CE3B9D4E625B6EC3403A /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -395,13 +516,14 @@
|
|||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 93E70FBCBD3EC11A08FAD800 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -410,13 +532,14 @@
|
|||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = C52233F0D1C275E97BE00A91 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -475,7 +598,6 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -525,8 +647,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -540,14 +661,16 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = KBM2RZ74VR;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -562,14 +685,16 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = KBM2RZ74VR;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.healthmanager.healthApp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.datalumina.YYA;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -611,6 +736,20 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1510"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -52,9 +70,9 @@
|
|||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Release"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = ""
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
|
|||||||
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@ import Flutter
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Health App</string>
|
<string>小脉健康</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>health_app</string>
|
<string>小脉健康</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
@@ -24,6 +26,39 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>需要使用蓝牙连接欧姆龙血压计以同步测量数据</string>
|
||||||
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
|
<string>需要使用蓝牙连接血压计</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>需要使用相机拍摄饮食照片和体检报告,以便 AI 分析和记录</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>需要保存图片到相册</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>需要访问相册以选取饮食照片、体检报告或个人头像</string>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -41,13 +76,5 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
||||||
<string>需要使用蓝牙连接欧姆龙血压计以同步测量数据</string>
|
|
||||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
|
||||||
<string>需要使用蓝牙连接血压计</string>
|
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
10
health_app/ios/Runner/Runner.entitlements
Normal file
10
health_app/ios/Runner/Runner.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string>Default</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -3,10 +3,10 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'local_database.dart';
|
import 'local_database.dart';
|
||||||
|
|
||||||
/// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
|
/// API 基础地址(生产模式)。本地开发可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
|
||||||
const String baseUrl = String.fromEnvironment(
|
const String baseUrl = String.fromEnvironment(
|
||||||
'API_BASE_URL',
|
'API_BASE_URL',
|
||||||
defaultValue: 'http://10.4.221.78:5000',
|
defaultValue: 'https://erpapi.datalumina.cn/xiaomai',
|
||||||
);
|
);
|
||||||
|
|
||||||
class ApiException implements Exception {
|
class ApiException implements Exception {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
import '../../core/navigation_provider.dart';
|
import '../../core/navigation_provider.dart';
|
||||||
import '../../providers/auth_provider.dart';
|
import '../../providers/auth_provider.dart';
|
||||||
@@ -63,6 +66,70 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apple 登录
|
||||||
|
Future<void> _appleSignIn() async {
|
||||||
|
if (_loading) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
_successMsg = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final credential = await SignInWithApple.getAppleIDCredential(
|
||||||
|
scopes: [
|
||||||
|
AppleIDAuthorizationScopes.fullName,
|
||||||
|
],
|
||||||
|
webAuthenticationOptions: WebAuthenticationOptions(
|
||||||
|
clientId: 'com.datalumina.YYA.signin',
|
||||||
|
redirectUri: Uri.parse(
|
||||||
|
'https://erpapi.datalumina.cn/xiaomai/api/auth/apple-login',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final identityToken = credential.identityToken;
|
||||||
|
if (identityToken == null) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = 'Apple 登录未返回身份信息,请重试';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = credential.givenName != null && credential.familyName != null
|
||||||
|
? '${credential.familyName}${credential.givenName}'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final err = await ref.read(authProvider.notifier).appleLogin(
|
||||||
|
identityToken,
|
||||||
|
credential.authorizationCode,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _loading = false);
|
||||||
|
if (err != null) {
|
||||||
|
setState(() => _error = err);
|
||||||
|
} else {
|
||||||
|
_goHome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
if (e is SignInWithAppleAuthorizationException) {
|
||||||
|
_error = 'Apple 登录已取消';
|
||||||
|
} else {
|
||||||
|
_error = 'Apple 登录失败: $e';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_agreed) {
|
if (!_agreed) {
|
||||||
final accepted = await _showAgreementDialog();
|
final accepted = await _showAgreementDialog();
|
||||||
@@ -489,6 +556,16 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
label: _isLogin ? '登录' : '注册',
|
label: _isLogin ? '登录' : '注册',
|
||||||
onTap: _loading ? null : _submit,
|
onTap: _loading ? null : _submit,
|
||||||
),
|
),
|
||||||
|
// Apple 登录按钮(仅 iOS/macOS)
|
||||||
|
if (_isLogin && (Platform.isIOS || Platform.isMacOS)) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SignInWithAppleButton(
|
||||||
|
onPressed: _loading ? () {} : () { _appleSignIn(); },
|
||||||
|
style: SignInWithAppleButtonStyle.whiteOutlined,
|
||||||
|
height: 48,
|
||||||
|
text: '通过 Apple 登录',
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() {
|
||||||
|
|||||||
@@ -207,6 +207,40 @@ class AuthNotifier extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apple 登录
|
||||||
|
Future<String?> appleLogin(String identityToken, String? authorizationCode, String? name) async {
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiClientProvider);
|
||||||
|
final response = await api.post(
|
||||||
|
'/api/auth/apple-login',
|
||||||
|
data: {
|
||||||
|
'identityToken': identityToken,
|
||||||
|
if (authorizationCode != null) 'authorizationCode': authorizationCode,
|
||||||
|
if (name != null && name.isNotEmpty) 'name': name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final data = response.data['data'];
|
||||||
|
if (data == null) return response.data['message'] ?? 'Apple 登录失败';
|
||||||
|
|
||||||
|
await api.saveTokens(data['accessToken'], data['refreshToken']);
|
||||||
|
final user = data['user'];
|
||||||
|
state = AuthState(
|
||||||
|
isLoggedIn: true,
|
||||||
|
isLoading: false,
|
||||||
|
user: UserInfo(
|
||||||
|
id: user['id'] ?? '',
|
||||||
|
phone: user['phone'] ?? '',
|
||||||
|
role: user['role'] ?? 'User',
|
||||||
|
name: user['name'],
|
||||||
|
avatarUrl: user['avatarUrl'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Apple 登录失败: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 登出
|
/// 登出
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
final api = ref.read(apiClientProvider);
|
final api = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
cli_config:
|
cli_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -617,18 +617,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
message_pack_dart:
|
message_pack_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -641,10 +641,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -901,6 +901,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
sign_in_with_apple:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sign_in_with_apple
|
||||||
|
sha256: e84a62e17b7e463abf0a64ce826c2cd1f0b72dff07b7b275e32d5302d76fb4c5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
|
sign_in_with_apple_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sign_in_with_apple_platform_interface
|
||||||
|
sha256: c2ef2ce6273fce0c61acd7e9ff5be7181e33d7aa2b66508b39418b786cca2119
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
sign_in_with_apple_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sign_in_with_apple_web
|
||||||
|
sha256: "2f7c38368f49e3f2043bca4b46a4a61aaae568c140a79aa0675dc59ad0ca49bc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
signalr_netcore:
|
signalr_netcore:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1062,26 +1086,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.3"
|
version: "1.31.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.11"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.12"
|
version: "0.6.17"
|
||||||
theme_extensions_builder_annotation:
|
theme_extensions_builder_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ dependencies:
|
|||||||
flutter_blue_plus: ^1.34.0
|
flutter_blue_plus: ^1.34.0
|
||||||
permission_handler: ^11.3.0
|
permission_handler: ^11.3.0
|
||||||
|
|
||||||
|
# Apple 登录
|
||||||
|
sign_in_with_apple: ^6.1.0
|
||||||
|
|
||||||
# 基础图标
|
# 基础图标
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
signalr_netcore: ^1.4.4
|
signalr_netcore: ^1.4.4
|
||||||
|
|||||||
Reference in New Issue
Block a user