Skip to content

Commit 784be48

Browse files
rmunnmyieye
andauthored
Add "Where's my project?" link to FW Lite and corresponding page to Lexbox (#1650)
* Added a "Where's my project?" page to Lexbox which displays different content based on whether the user has FW Lite feature flag or not * If user has feature flag, give instructions for how to get project * If user doesn't have feature flag, a button allows user to send an email to Lexbox admins to ask for the feature flag to be added * Added a link to FW Lite that will link to the Lexbox page --------- Co-authored-by: Tim Haasdyk <[email protected]>
1 parent bad4439 commit 784be48

File tree

14 files changed

+220
-4
lines changed

14 files changed

+220
-4
lines changed

backend/LexBoxApi/GraphQL/UserMutations.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.ComponentModel.DataAnnotations;
1+
using System.ComponentModel.DataAnnotations;
22
using System.Security.Cryptography;
33
using LexBoxApi.Auth;
44
using LexBoxApi.Auth.Attributes;
@@ -36,6 +36,35 @@ public record CreateGuestUserByAdminInput(
3636
string PasswordHash,
3737
int PasswordStrength,
3838
Guid? OrgId);
39+
public record SendFWLiteBetaRequestEmailInput(Guid UserId, string Name);
40+
public enum SendFWLiteBetaRequestEmailResult
41+
{
42+
UserAlreadyInBeta,
43+
BetaAccessRequestSent,
44+
};
45+
46+
[Error<NotFoundException>]
47+
[UseMutationConvention]
48+
public async Task<SendFWLiteBetaRequestEmailResult> SendFWLiteBetaRequestEmail(
49+
LoggedInContext loggedInContext,
50+
SendFWLiteBetaRequestEmailInput input,
51+
LexBoxDbContext dbContext,
52+
LexAuthService lexAuthService,
53+
IEmailService emailService
54+
)
55+
{
56+
if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException();
57+
var user = await dbContext.Users.FindAsync(input.UserId);
58+
NotFoundException.ThrowIfNull(user);
59+
if (user.FeatureFlags.Contains(FeatureFlag.FwLiteBeta))
60+
{
61+
if (!loggedInContext.User.FeatureFlags.Contains(FeatureFlag.FwLiteBeta))
62+
await lexAuthService.RefreshUser();
63+
return SendFWLiteBetaRequestEmailResult.UserAlreadyInBeta;
64+
}
65+
await emailService.SendJoinFwLiteBetaEmail(user);
66+
return SendFWLiteBetaRequestEmailResult.BetaAccessRequestSent;
67+
}
3968

4069
[Error<NotFoundException>]
4170
[Error<DbError>]

backend/LexBoxApi/Services/Email/EmailTemplates.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public enum EmailTemplate
2323
CreateProjectRequest,
2424
ApproveProjectRequest,
2525
UserAdded,
26+
JoinFwLiteBetaRequest,
2627
}
2728

2829
public record ForgotPasswordEmail(string Name, string ResetUrl, TimeSpan lifetime) : EmailTemplateBase(EmailTemplate.ForgotPassword);
@@ -41,3 +42,4 @@ public record CreateProjectRequestUser(string Name, string Email);
4142
public record CreateProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.CreateProjectRequest);
4243
public record ApproveProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.ApproveProjectRequest);
4344
public record UserAddedEmail(string Name, string Email, string ProjectName, string ProjectCode) : EmailTemplateBase(EmailTemplate.UserAdded);
45+
public record JoinFwLiteBetaEmail(string Name, string Email) : EmailTemplateBase(EmailTemplate.JoinFwLiteBetaRequest);

backend/LexBoxApi/Services/Email/IEmailService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ public Task SendCreateAccountWithProjectEmail(
5757
public Task SendCreateProjectRequestEmail(LexAuthUser user, CreateProjectInput projectInput);
5858
public Task SendApproveProjectRequestEmail(User user, CreateProjectInput projectInput);
5959
public Task SendUserAddedEmail(User user, string projectName, string projectCode);
60+
public Task SendJoinFwLiteBetaEmail(User user);
6061
public Task SendEmailAsync(MimeMessage message);
6162
}

backend/LexBoxApi/Services/EmailService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ public async Task SendUserAddedEmail(User user, string projectName, string proje
215215
await RenderEmail(email, new UserAddedEmail(user.Name, user.Email!, projectName, projectCode), user.LocalizationCode);
216216
await SendEmailWithRetriesAsync(email);
217217
}
218+
219+
public async Task SendJoinFwLiteBetaEmail(User user)
220+
{
221+
var email = StartUserEmail("Lexbox Support", "[email protected]"); // TODO: Get from environment
222+
await RenderEmail(email, new JoinFwLiteBetaEmail(user.Name, user.Email ?? ""), user.LocalizationCode);
223+
await SendEmailWithRetriesAsync(email);
224+
}
225+
218226
public async Task SendEmailAsync(MimeMessage message)
219227
{
220228
message.From.Add(MailboxAddress.Parse(_emailConfig.From));

frontend/schema.graphql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ type Mutation {
275275
removeProjectMember(input: RemoveProjectMemberInput!): RemoveProjectMemberPayload! @cost(weight: "10")
276276
deleteDraftProject(input: DeleteDraftProjectInput!): DeleteDraftProjectPayload! @authorize(policy: "AdminRequiredPolicy") @cost(weight: "10")
277277
softDeleteProject(input: SoftDeleteProjectInput!): SoftDeleteProjectPayload! @cost(weight: "10")
278+
sendFWLiteBetaRequestEmail(input: SendFWLiteBetaRequestEmailInput!): SendFWLiteBetaRequestEmailPayload! @cost(weight: "10")
278279
changeUserAccountBySelf(input: ChangeUserAccountBySelfInput!): ChangeUserAccountBySelfPayload! @cost(weight: "10")
279280
changeUserAccountByAdmin(input: ChangeUserAccountByAdminInput!): ChangeUserAccountByAdminPayload! @authorize(policy: "AdminRequiredPolicy") @cost(weight: "10")
280281
sendNewVerificationEmailByAdmin(input: SendNewVerificationEmailByAdminInput!): SendNewVerificationEmailByAdminPayload! @authorize(policy: "AdminRequiredPolicy") @cost(weight: "10")
@@ -493,6 +494,11 @@ type RequiredError implements Error {
493494
message: String!
494495
}
495496

497+
type SendFWLiteBetaRequestEmailPayload {
498+
sendFWLiteBetaRequestEmailResult: SendFWLiteBetaRequestEmailResult
499+
errors: [SendFWLiteBetaRequestEmailError!]
500+
}
501+
496502
type SendNewVerificationEmailByAdminPayload {
497503
user: User
498504
errors: [SendNewVerificationEmailByAdminError!]
@@ -645,6 +651,8 @@ union LeaveProjectError = NotFoundError | LastMemberCantLeaveError
645651

646652
union RemoveProjectFromOrgError = DbError | NotFoundError
647653

654+
union SendFWLiteBetaRequestEmailError = NotFoundError
655+
648656
union SendNewVerificationEmailByAdminError = NotFoundError | DbError | InvalidOperationError
649657

650658
union SetOrgMemberRoleError = DbError | NotFoundError | OrgMemberInvitedByEmail | OrgMembersMustBeVerified | OrgMembersMustBeVerifiedForRole
@@ -1094,6 +1102,11 @@ input RetentionPolicyOperationFilterInput {
10941102
nin: [RetentionPolicy!] @cost(weight: "10")
10951103
}
10961104

1105+
input SendFWLiteBetaRequestEmailInput {
1106+
userId: UUID!
1107+
name: String!
1108+
}
1109+
10971110
input SendNewVerificationEmailByAdminInput {
10981111
userId: UUID!
10991112
}
@@ -1282,6 +1295,11 @@ enum RetentionPolicy {
12821295
TRAINING
12831296
}
12841297

1298+
enum SendFWLiteBetaRequestEmailResult {
1299+
USER_ALREADY_IN_BETA
1300+
BETA_ACCESS_REQUEST_SENT
1301+
}
1302+
12851303
enum SortEnumType {
12861304
ASC
12871305
DESC

frontend/src/lib/email/Email.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
const lexboxLogo = 'https://lexbox.org/images/logo-dark.png';
66
interface Props {
77
subject: string;
8-
name: string;
8+
name?: string;
99
children?: Snippet;
1010
}
1111
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import Email from '$lib/email/Email.svelte';
3+
import t from '$lib/i18n';
4+
5+
export let name: string;
6+
export let email: string;
7+
export let baseUrl: string;
8+
let approveUrl = new URL(`/admin/?userSearch=${encodeURIComponent(email)}`, baseUrl);
9+
</script>
10+
11+
<Email subject={$t('emails.join_fw_lite_beta_request_email.subject', { name })}>
12+
<mj-text>{$t('emails.join_fw_lite_beta_request_email.body', { name })}</mj-text>
13+
<mj-button href={approveUrl}>{$t('emails.join_fw_lite_beta_request_email.approve_button')}</mj-button>
14+
</Email>

frontend/src/lib/i18n/locales/en.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,18 @@ If you don't see a dialog or already closed it, click the button below:",
668668
"admin": "Admin",
669669
"user": "User"
670670
},
671+
"where_is_my_project": {
672+
"title": "FieldWorks Lite - Where's my project?",
673+
"body": "If you don't see your project in FieldWorks Lite, navigate to the project here on Lexbox and look for the \"Try FieldWorks Lite?\" button\n\
674+
near the top of the page. Use that button to make your project available in FieldWorks Lite. This process may take a few minutes. When\n\
675+
it's done, you should see your project in FieldWorks Lite and be able to use it.",
676+
"user_not_in_beta": "FieldWorks Lite is currently in a beta testing phase. \n\
677+
Only selected early access users can download Lexbox projects in FieldWorks Lite. \n\
678+
Click the button below to request to join the early access program. It may take a few days for us to respond.",
679+
"request_beta_access": "Request early access to FieldWorks Lite",
680+
"access_request_sent": "Your request to join the early access program has been submitted",
681+
"already_in_beta": "You're already in the early access program",
682+
},
671683
"errors": {
672684
"apology": "Woops, something went wrong on our end. Sorry!",
673685
"mail_us_at": "If you're stuck, let us know about this at",
@@ -736,6 +748,11 @@ If you don't see a dialog or already closed it, click the button below:",
736748
"heading": "The project you requested, {projectName}, has been approved and created.",
737749
"view_button": "View Project"
738750
},
751+
"join_fw_lite_beta_request_email": {
752+
"subject": "FW Lite Beta join request: {name}",
753+
"body": "User {name} requested to join the FW Lite beta. Click below to approve this request.",
754+
"approve_button": "Approve Request"
755+
},
739756
"user_added": {
740757
"subject": "You joined project: {projectName}!",
741758
"body": "You have been added to the project: {projectName}.",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
import { page } from '$app/state';
4+
import type { FeatureFlag } from '$lib/gql/types';
5+
import { hasFeatureFlag } from '$lib/user';
6+
7+
interface Props {
8+
flag: FeatureFlag | keyof typeof FeatureFlag;
9+
children?: Snippet;
10+
hasFlagContent?: Snippet;
11+
missingFlagContent?: Snippet;
12+
}
13+
14+
let { flag, missingFlagContent, hasFlagContent, children }: Props = $props();
15+
</script>
16+
17+
<!-- eslint-disable-next-line @typescript-eslint/no-unsafe-argument -->
18+
{#if hasFeatureFlag(page.data.user, flag)}
19+
{@render (hasFlagContent ?? children)?.()}
20+
{:else}
21+
{@render missingFlagContent?.()}
22+
{/if}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script lang="ts">
2+
import { TitlePage } from '$lib/layout';
3+
import t from '$lib/i18n';
4+
import Markdown from 'svelte-exmarkdown';
5+
import FeatureFlagAlternateContent from '$lib/layout/FeatureFlagAlternateContent.svelte';
6+
import Button from '$lib/forms/Button.svelte';
7+
import { page } from '$app/state';
8+
import { _sendFWLiteBetaRequestEmail } from './+page';
9+
import type { UUID } from 'crypto';
10+
import { SendFwLiteBetaRequestEmailResult } from '$lib/gql/generated/graphql';
11+
import { useNotifications } from '$lib/notify';
12+
13+
const { notifySuccess } = useNotifications();
14+
15+
let requesting = $state(false);
16+
17+
async function requestBetaAccess(): Promise<void> {
18+
requesting = true;
19+
try {
20+
const gqlResult = await _sendFWLiteBetaRequestEmail(page.data.user.id as UUID, page.data.user.name);
21+
if (gqlResult.error) {
22+
if (gqlResult.error.byType('NotFoundError')) {
23+
console.log('User not found, no dialog shown');
24+
}
25+
}
26+
const result = gqlResult.data?.sendFWLiteBetaRequestEmail.sendFWLiteBetaRequestEmailResult;
27+
if (result === SendFwLiteBetaRequestEmailResult.BetaAccessRequestSent) {
28+
notifySuccess($t('where_is_my_project.access_request_sent'));
29+
}
30+
if (result === SendFwLiteBetaRequestEmailResult.UserAlreadyInBeta) {
31+
notifySuccess($t('where_is_my_project.already_in_beta'));
32+
}
33+
} finally {
34+
requesting = false;
35+
}
36+
}
37+
</script>
38+
39+
<TitlePage title={$t('where_is_my_project.title')}>
40+
<div class="prose text-lg">
41+
<FeatureFlagAlternateContent flag="FwLiteBeta">
42+
{#snippet hasFlagContent()}
43+
<Markdown md={$t('where_is_my_project.body')} />
44+
{/snippet}
45+
{#snippet missingFlagContent()}
46+
<Markdown md={$t('where_is_my_project.user_not_in_beta')} />
47+
<div class="text-center">
48+
<Button loading={requesting} variant="btn-primary" onclick={requestBetaAccess}>{$t('where_is_my_project.request_beta_access')}</Button>
49+
</div>
50+
{/snippet}
51+
</FeatureFlagAlternateContent>
52+
</div>
53+
</TitlePage>

0 commit comments

Comments
 (0)