mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: complete UIUX for AI and limits
This commit is contained in:
parent
61419e305c
commit
e9715abe90
@ -106,6 +106,7 @@ class SettingsBillingBloc
|
|||||||
addSubscription: (plan) async {
|
addSubscription: (plan) async {
|
||||||
final result =
|
final result =
|
||||||
await _userService.createSubscription(workspaceId, plan);
|
await _userService.createSubscription(workspaceId, plan);
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(link) => afLaunchUrlString(link.paymentLink),
|
(link) => afLaunchUrlString(link.paymentLink),
|
||||||
(f) => Log.error(f.msg, f),
|
(f) => Log.error(f.msg, f),
|
||||||
|
@ -28,8 +28,10 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
|
|
||||||
on<SettingsPlanEvent>((event, emit) async {
|
on<SettingsPlanEvent>((event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
started: (withSuccessfulUpgrade) async {
|
started: (withSuccessfulUpgrade, shouldLoad) async {
|
||||||
|
if (shouldLoad) {
|
||||||
emit(const SettingsPlanState.loading());
|
emit(const SettingsPlanState.loading());
|
||||||
|
}
|
||||||
|
|
||||||
final snapshots = await Future.wait([
|
final snapshots = await Future.wait([
|
||||||
_service.getWorkspaceUsage(),
|
_service.getWorkspaceUsage(),
|
||||||
@ -109,7 +111,12 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(SettingsPlanEvent.started(withSuccessfulUpgrade: plan));
|
add(
|
||||||
|
SettingsPlanEvent.started(
|
||||||
|
withSuccessfulUpgrade: plan,
|
||||||
|
shouldLoad: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -142,6 +149,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
class SettingsPlanEvent with _$SettingsPlanEvent {
|
class SettingsPlanEvent with _$SettingsPlanEvent {
|
||||||
const factory SettingsPlanEvent.started({
|
const factory SettingsPlanEvent.started({
|
||||||
@Default(null) SubscriptionPlanPB? withSuccessfulUpgrade,
|
@Default(null) SubscriptionPlanPB? withSuccessfulUpgrade,
|
||||||
|
@Default(true) bool shouldLoad,
|
||||||
}) = _Started;
|
}) = _Started;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||||
|
@ -367,8 +367,9 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VSpace(16),
|
const VSpace(16),
|
||||||
Column(
|
SeparatedColumn(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
separatorBuilder: () => const VSpace(4),
|
||||||
children: [
|
children: [
|
||||||
if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[
|
if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[
|
||||||
_ToggleMore(
|
_ToggleMore(
|
||||||
@ -383,10 +384,40 @@ class _PlanUsageSummary extends StatelessWidget {
|
|||||||
SubscriptionPlanPB.Pro,
|
SubscriptionPlanPB.Pro,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await Future.delayed(
|
await Future.delayed(const Duration(seconds: 2), () {});
|
||||||
const Duration(seconds: 2),
|
},
|
||||||
() {},
|
),
|
||||||
|
],
|
||||||
|
if (!subscriptionInfo.hasAIMax) ...[
|
||||||
|
_ToggleMore(
|
||||||
|
value: false,
|
||||||
|
label: LocaleKeys.settings_planPage_planUsage_aiMaxToggle.tr(),
|
||||||
|
badgeLabel:
|
||||||
|
LocaleKeys.settings_planPage_planUsage_aiMaxBadge.tr(),
|
||||||
|
onTap: () async {
|
||||||
|
context.read<SettingsPlanBloc>().add(
|
||||||
|
const SettingsPlanEvent.addSubscription(
|
||||||
|
SubscriptionPlanPB.AiMax,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
await Future.delayed(const Duration(seconds: 2), () {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (!subscriptionInfo.hasAIOnDevice) ...[
|
||||||
|
_ToggleMore(
|
||||||
|
value: false,
|
||||||
|
label: LocaleKeys.settings_planPage_planUsage_aiOnDeviceToggle
|
||||||
|
.tr(),
|
||||||
|
badgeLabel:
|
||||||
|
LocaleKeys.settings_planPage_planUsage_aiOnDeviceBadge.tr(),
|
||||||
|
onTap: () async {
|
||||||
|
context.read<SettingsPlanBloc>().add(
|
||||||
|
const SettingsPlanEvent.addSubscription(
|
||||||
|
SubscriptionPlanPB.AiLocal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await Future.delayed(const Duration(seconds: 2), () {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -417,8 +448,6 @@ class _UsageBox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isLM = Theme.of(context).isLightMode;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -429,18 +458,8 @@ class _UsageBox extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (unlimited) ...[
|
if (unlimited) ...[
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
DecoratedBox(
|
Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
border: Border.all(
|
|
||||||
color: isLM ? const Color(0xFFE8E2EE) : const Color(0xFF9C00FB),
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 4,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -457,7 +476,6 @@ class _UsageBox extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
] else ...[
|
] else ...[
|
||||||
_PlanProgressIndicator(label: label, progress: value),
|
_PlanProgressIndicator(label: label, progress: value),
|
||||||
],
|
],
|
||||||
@ -569,7 +587,9 @@ class _PlanProgressIndicator extends StatelessWidget {
|
|||||||
widthFactor: progress,
|
widthFactor: progress,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary,
|
color: progress >= 1
|
||||||
|
? theme.colorScheme.error
|
||||||
|
: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -613,6 +633,8 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isLM = Theme.of(context).isLightMode;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 220,
|
height: 220,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -621,11 +643,9 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isActive ? const Color(0xFF9C00FB) : const Color(0xFFBDBDBD),
|
color: isActive ? const Color(0xFFBDBDBD) : const Color(0xFF9C00FB),
|
||||||
),
|
),
|
||||||
color: isActive
|
color: const Color(0xFFF7F8FC).withOpacity(0.05),
|
||||||
? const Color(0xFFF7F8FC).withOpacity(0.05)
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -673,22 +693,36 @@ class _AddOnBox extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyTextButton(
|
child: FlowyTextButton(
|
||||||
buttonText,
|
buttonText,
|
||||||
|
heading: isActive
|
||||||
|
? const FlowySvg(
|
||||||
|
FlowySvgs.check_circle_outlined_s,
|
||||||
|
color: Color(0xFF9C00FB),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
||||||
fillColor:
|
fillColor: isActive
|
||||||
isActive ? const Color(0xFFE8E2EE) : Colors.transparent,
|
? const Color(0xFFE8E2EE)
|
||||||
|
: isLM
|
||||||
|
? Colors.transparent
|
||||||
|
: const Color(0xFF5C3699),
|
||||||
constraints: const BoxConstraints(minWidth: 115),
|
constraints: const BoxConstraints(minWidth: 115),
|
||||||
radius: Corners.s16Border,
|
radius: Corners.s16Border,
|
||||||
hoverColor: isActive
|
hoverColor: isActive
|
||||||
? const Color(0xFFE8E2EE)
|
? const Color(0xFFE8E2EE)
|
||||||
: const Color(0xFF5C3699),
|
: isLM
|
||||||
fontColor: const Color(0xFF5C3699),
|
? const Color(0xFF5C3699)
|
||||||
|
: const Color(0xFF4d3472),
|
||||||
|
fontColor:
|
||||||
|
isLM || isActive ? const Color(0xFF5C3699) : Colors.white,
|
||||||
fontHoverColor:
|
fontHoverColor:
|
||||||
isActive ? const Color(0xFF5C3699) : Colors.white,
|
isActive ? const Color(0xFF5C3699) : Colors.white,
|
||||||
borderColor: isActive
|
borderColor: isActive
|
||||||
? const Color(0xFFE8E2EE)
|
? const Color(0xFFE8E2EE)
|
||||||
: const Color(0xFF5C3699),
|
: isLM
|
||||||
|
? const Color(0xFF5C3699)
|
||||||
|
: const Color(0xFF4d3472),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
onPressed: isActive
|
onPressed: isActive
|
||||||
? null
|
? null
|
||||||
|
@ -120,7 +120,10 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud();
|
return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud();
|
||||||
}
|
}
|
||||||
case SettingsPage.member:
|
case SettingsPage.member:
|
||||||
return WorkspaceMembersPage(userProfile: user);
|
return WorkspaceMembersPage(
|
||||||
|
userProfile: user,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
);
|
||||||
case SettingsPage.plan:
|
case SettingsPage.plan:
|
||||||
return SettingsPlanView(workspaceId: workspaceId, user: user);
|
return SettingsPlanView(workspaceId: workspaceId, user: user);
|
||||||
case SettingsPage.billing:
|
case SettingsPage.billing:
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -24,13 +27,14 @@ class WorkspaceMemberBloc
|
|||||||
extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {
|
extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {
|
||||||
WorkspaceMemberBloc({
|
WorkspaceMemberBloc({
|
||||||
required this.userProfile,
|
required this.userProfile,
|
||||||
|
String? workspaceId,
|
||||||
this.workspace,
|
this.workspace,
|
||||||
}) : _userBackendService = UserBackendService(userId: userProfile.id),
|
}) : _userBackendService = UserBackendService(userId: userProfile.id),
|
||||||
super(WorkspaceMemberState.initial()) {
|
super(WorkspaceMemberState.initial()) {
|
||||||
on<WorkspaceMemberEvent>((event, emit) async {
|
on<WorkspaceMemberEvent>((event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
initial: () async {
|
initial: () async {
|
||||||
await _setCurrentWorkspaceId();
|
await _setCurrentWorkspaceId(workspaceId);
|
||||||
|
|
||||||
final result = await _userBackendService.getWorkspaceMembers(
|
final result = await _userBackendService.getWorkspaceMembers(
|
||||||
_workspaceId,
|
_workspaceId,
|
||||||
@ -135,9 +139,7 @@ class WorkspaceMemberBloc
|
|||||||
(s) => state.members.map((e) {
|
(s) => state.members.map((e) {
|
||||||
if (e.email == email) {
|
if (e.email == email) {
|
||||||
e.freeze();
|
e.freeze();
|
||||||
return e.rebuild((p0) {
|
return e.rebuild((p0) => p0.role = role);
|
||||||
p0.role = role;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@ -153,7 +155,26 @@ class WorkspaceMemberBloc
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
upgradePlan: () {},
|
updateSubscriptionInfo: (info) async =>
|
||||||
|
emit(state.copyWith(subscriptionInfo: info)),
|
||||||
|
upgradePlan: () async {
|
||||||
|
final plan = state.subscriptionInfo?.plan;
|
||||||
|
if (plan == null) {
|
||||||
|
return Log.error('Failed to upgrade plan: plan is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan == WorkspacePlanPB.FreePlan) {
|
||||||
|
final checkoutLink = await _userBackendService.createSubscription(
|
||||||
|
_workspaceId,
|
||||||
|
SubscriptionPlanPB.Pro,
|
||||||
|
);
|
||||||
|
|
||||||
|
checkoutLink.fold(
|
||||||
|
(pl) => afLaunchUrlString(pl.paymentLink),
|
||||||
|
(f) => Log.error('Failed to create subscription: ${f.msg}', f),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -179,9 +200,11 @@ class WorkspaceMemberBloc
|
|||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setCurrentWorkspaceId() async {
|
Future<void> _setCurrentWorkspaceId(String? workspaceId) async {
|
||||||
if (workspace != null) {
|
if (workspace != null) {
|
||||||
_workspaceId = workspace!.workspaceId;
|
_workspaceId = workspace!.workspaceId;
|
||||||
|
} else if (workspaceId != null && workspaceId.isNotEmpty) {
|
||||||
|
_workspaceId = workspaceId;
|
||||||
} else {
|
} else {
|
||||||
final currentWorkspace = await FolderEventReadCurrentWorkspace().send();
|
final currentWorkspace = await FolderEventReadCurrentWorkspace().send();
|
||||||
currentWorkspace.fold((s) {
|
currentWorkspace.fold((s) {
|
||||||
@ -192,6 +215,20 @@ class WorkspaceMemberBloc
|
|||||||
_workspaceId = '';
|
_workspaceId = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unawaited(_fetchWorkspaceSubscriptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We fetch workspace subscription info lazily as it's not needed in the first
|
||||||
|
// render of the page.
|
||||||
|
Future<void> _fetchWorkspaceSubscriptionInfo() async {
|
||||||
|
final result =
|
||||||
|
await UserBackendService.getWorkspaceSubscriptionInfo(_workspaceId);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(info) => add(WorkspaceMemberEvent.updateSubscriptionInfo(info)),
|
||||||
|
(f) => Log.error('Failed to fetch subscription info: ${f.msg}', f),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +247,9 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent {
|
|||||||
String email,
|
String email,
|
||||||
AFRolePB role,
|
AFRolePB role,
|
||||||
) = UpdateWorkspaceMember;
|
) = UpdateWorkspaceMember;
|
||||||
|
const factory WorkspaceMemberEvent.updateSubscriptionInfo(
|
||||||
|
WorkspaceSubscriptionInfoPB subscriptionInfo,
|
||||||
|
) = UpdateSubscriptionInfo;
|
||||||
|
|
||||||
const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan;
|
const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan;
|
||||||
}
|
}
|
||||||
@ -244,6 +284,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState {
|
|||||||
@Default(AFRolePB.Guest) AFRolePB myRole,
|
@Default(AFRolePB.Guest) AFRolePB myRole,
|
||||||
@Default(null) WorkspaceMemberActionResult? actionResult,
|
@Default(null) WorkspaceMemberActionResult? actionResult,
|
||||||
@Default(true) bool isLoading,
|
@Default(true) bool isLoading,
|
||||||
|
@Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo,
|
||||||
}) = _WorkspaceMemberState;
|
}) = _WorkspaceMemberState;
|
||||||
|
|
||||||
factory WorkspaceMemberState.initial() => const WorkspaceMemberState();
|
factory WorkspaceMemberState.initial() => const WorkspaceMemberState();
|
||||||
@ -258,6 +299,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState {
|
|||||||
return other is WorkspaceMemberState &&
|
return other is WorkspaceMemberState &&
|
||||||
other.members == members &&
|
other.members == members &&
|
||||||
other.myRole == myRole &&
|
other.myRole == myRole &&
|
||||||
|
other.subscriptionInfo == subscriptionInfo &&
|
||||||
identical(other.actionResult, actionResult);
|
identical(other.actionResult, actionResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
@ -13,7 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
@ -21,9 +22,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:string_validator/string_validator.dart';
|
import 'package:string_validator/string_validator.dart';
|
||||||
|
|
||||||
class WorkspaceMembersPage extends StatelessWidget {
|
class WorkspaceMembersPage extends StatelessWidget {
|
||||||
const WorkspaceMembersPage({super.key, required this.userProfile});
|
const WorkspaceMembersPage({
|
||||||
|
super.key,
|
||||||
|
required this.userProfile,
|
||||||
|
required this.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
final UserProfilePB userProfile;
|
final UserProfilePB userProfile;
|
||||||
|
final String workspaceId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -34,13 +40,16 @@ class WorkspaceMembersPage extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
title: LocaleKeys.settings_appearance_members_title.tr(),
|
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||||
|
autoSeparate: false,
|
||||||
children: [
|
children: [
|
||||||
if (state.actionResult != null)
|
if (state.actionResult != null) ...[
|
||||||
_showMemberLimitWarning(
|
_showMemberLimitWarning(context, state),
|
||||||
context,
|
const VSpace(16),
|
||||||
state.actionResult!,
|
],
|
||||||
),
|
if (state.myRole.canInvite) ...[
|
||||||
if (state.myRole.canInvite) const _InviteMember(),
|
const _InviteMember(),
|
||||||
|
const SettingsCategorySpacer(),
|
||||||
|
],
|
||||||
if (state.members.isNotEmpty)
|
if (state.members.isNotEmpty)
|
||||||
_MemberList(
|
_MemberList(
|
||||||
members: state.members,
|
members: state.members,
|
||||||
@ -56,78 +65,98 @@ class WorkspaceMembersPage extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _showMemberLimitWarning(
|
Widget _showMemberLimitWarning(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WorkspaceMemberActionResult result,
|
WorkspaceMemberState state,
|
||||||
) {
|
) {
|
||||||
final isLM = Theme.of(context).isLightMode;
|
// We promise that state.actionResult != null before calling
|
||||||
|
// this method
|
||||||
|
final actionResult = state.actionResult!.result;
|
||||||
|
final actionType = state.actionResult!.actionType;
|
||||||
|
|
||||||
final actionType = result.actionType;
|
debugPrint("Plan: ${state.subscriptionInfo?.plan}");
|
||||||
final actionResult = result.result;
|
|
||||||
|
|
||||||
if (actionType == WorkspaceMemberActionType.invite &&
|
if (actionType == WorkspaceMemberActionType.invite &&
|
||||||
actionResult.isFailure) {
|
actionResult.isFailure) {
|
||||||
final error = actionResult.getFailure().code;
|
final error = actionResult.getFailure().code;
|
||||||
if (error == ErrorCode.WorkspaceMemberLimitExceeded) {
|
if (error == ErrorCode.WorkspaceMemberLimitExceeded) {
|
||||||
return DecoratedBox(
|
return Row(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isLM
|
|
||||||
? const Color(0xFFFFF4E5)
|
|
||||||
: const Color.fromARGB(255, 255, 200, 125),
|
|
||||||
borderRadius: Corners.s8Border,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 6,
|
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
FlowySvg(
|
const FlowySvg(
|
||||||
FlowySvgs.warning_m,
|
FlowySvgs.warning_s,
|
||||||
color: isLM
|
blendMode: BlendMode.dst,
|
||||||
? const Color(0xFFEF6C00)
|
size: Size.square(20),
|
||||||
: const Color.fromARGB(255, 160, 75, 0),
|
|
||||||
),
|
),
|
||||||
const HSpace(12),
|
const HSpace(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText(
|
child: RichText(
|
||||||
LocaleKeys.settings_appearance_members_memberLimitExceeded
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
if (state.subscriptionInfo?.plan ==
|
||||||
|
WorkspacePlanPB.ProPlan) ...[
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys
|
||||||
|
.settings_appearance_members_memberLimitExceededPro
|
||||||
.tr(),
|
.tr(),
|
||||||
color: const Color(0xFF663C00),
|
style: TextStyle(
|
||||||
maxLines: 3,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AFThemeExtension.of(context).strongText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MouseRegion(
|
WidgetSpan(
|
||||||
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {},
|
// Hardcoded support email, in the future we might
|
||||||
child: DecoratedBox(
|
// want to add this to an environment variable
|
||||||
decoration: BoxDecoration(
|
onTap: () async => afLaunchUrlString(
|
||||||
border: Border.all(
|
'mailto:support@appflowy.io',
|
||||||
color: const Color(0xFF5F2120),
|
|
||||||
),
|
|
||||||
borderRadius: Corners.s4Border,
|
|
||||||
),
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 5,
|
|
||||||
),
|
),
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
// TODO(Mathias): Localization
|
LocaleKeys
|
||||||
'UPGRADE',
|
.settings_appearance_members_memberLimitExceededProContact
|
||||||
fontSize: 13,
|
.tr(),
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: 14,
|
||||||
color: Color(0xFF663C00),
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
] else ...[
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys
|
||||||
|
.settings_appearance_members_memberLimitExceeded
|
||||||
|
.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AFThemeExtension.of(context).strongText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => context
|
||||||
|
.read<WorkspaceMemberBloc>()
|
||||||
|
.add(const WorkspaceMemberEvent.upgradePlan()),
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys
|
||||||
|
.settings_appearance_members_memberLimitExceededUpgrade
|
||||||
|
.tr(),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
frontend/resources/flowy_icons/16x/warning.svg
Normal file
5
frontend/resources/flowy_icons/16x/warning.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8.00033" cy="7.99984" r="7.33333" fill="#FF811A"/>
|
||||||
|
<path d="M7.99967 4.6665C7.63148 4.6665 7.33301 4.96498 7.33301 5.33317V8.6665C7.33301 9.03469 7.63148 9.33317 7.99967 9.33317C8.36786 9.33317 8.66634 9.03469 8.66634 8.6665V5.33317C8.66634 4.96498 8.36786 4.6665 7.99967 4.6665Z" fill="white"/>
|
||||||
|
<path d="M7.99967 11.3332C8.36786 11.3332 8.66634 11.0347 8.66634 10.6665C8.66634 10.2983 8.36786 9.99984 7.99967 9.99984C7.63148 9.99984 7.33301 10.2983 7.33301 10.6665C7.33301 11.0347 7.63148 11.3332 7.99967 11.3332Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 646 B |
@ -1,5 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11.0003 5.949L17.9028 17.8748H4.09783L11.0003 5.949ZM11.0003 2.2915L0.916992 19.7082H21.0837L11.0003 2.2915Z" fill="#EF6C00"/>
|
|
||||||
<path d="M11.917 15.1248H10.0837V16.9582H11.917V15.1248Z" fill="#EF6C00"/>
|
|
||||||
<path d="M11.917 9.62484H10.0837V14.2082H11.917V9.62484Z" fill="#EF6C00"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 390 B |
@ -646,7 +646,11 @@
|
|||||||
"aiResponseUsage": "{} of {}",
|
"aiResponseUsage": "{} of {}",
|
||||||
"unlimitedAILabel": "Unlimited responses",
|
"unlimitedAILabel": "Unlimited responses",
|
||||||
"proBadge": "Pro",
|
"proBadge": "Pro",
|
||||||
|
"aiMaxBadge": "AI Max",
|
||||||
|
"aiOnDeviceBadge": "AI On-device",
|
||||||
"memberProToggle": "More members & unlimited AI",
|
"memberProToggle": "More members & unlimited AI",
|
||||||
|
"aiMaxToggle": "Unlimited AI responses",
|
||||||
|
"aiOnDeviceToggle": "On-device AI for ultimate privacy",
|
||||||
"aiCredit": {
|
"aiCredit": {
|
||||||
"title": "Add @:appName AI Credit",
|
"title": "Add @:appName AI Credit",
|
||||||
"price": "5$",
|
"price": "5$",
|
||||||
@ -717,15 +721,15 @@
|
|||||||
"renewLabel": "Renew",
|
"renewLabel": "Renew",
|
||||||
"aiMax": {
|
"aiMax": {
|
||||||
"label": "AI Max",
|
"label": "AI Max",
|
||||||
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
"description": "Unlock unlimited AI and advanced models",
|
||||||
"activeDescription": "Next invoice due on {}",
|
"activeDescription": "Next invoice due on {}",
|
||||||
"canceledDescription": "AI Max will be removed on {}"
|
"canceledDescription": "AI Max will be available until {}"
|
||||||
},
|
},
|
||||||
"aiOnDevice": {
|
"aiOnDevice": {
|
||||||
"label": "AI On-device",
|
"label": "AI On-device",
|
||||||
"description": "US$8 /user per month billed annually or US$10 billed monthly",
|
"description": "Unlock unlimited AI offline on your device",
|
||||||
"activeDescription": "Next invoice due on {}",
|
"activeDescription": "Next invoice due on {}",
|
||||||
"canceledDescription": "AI On-device will be removed on {}"
|
"canceledDescription": "AI On-device will be available until {}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -951,7 +955,10 @@
|
|||||||
"one": "{} member",
|
"one": "{} member",
|
||||||
"other": "{} members"
|
"other": "{} members"
|
||||||
},
|
},
|
||||||
"memberLimitExceeded": "Member limit reached. Upgrade to invite additional members.",
|
"memberLimitExceeded": "Member limit reached, to invite more members, please ",
|
||||||
|
"memberLimitExceededUpgrade": "upgrade",
|
||||||
|
"memberLimitExceededPro": "Member limit reached, if you require more members contact ",
|
||||||
|
"memberLimitExceededProContact": "support@appflowy.io",
|
||||||
"failedToAddMember": "Failed to add member",
|
"failedToAddMember": "Failed to add member",
|
||||||
"addMemberSuccess": "Member added successfully",
|
"addMemberSuccess": "Member added successfully",
|
||||||
"removeMember": "Remove Member",
|
"removeMember": "Remove Member",
|
||||||
|
Loading…
Reference in New Issue
Block a user