Skip to content

Commit b7d035b

Browse files
Merge pull request #530 from gridaco/enterprise
Enterprise 260207-001
2 parents feb62e5 + 2a303e3 commit b7d035b

File tree

7 files changed

+392
-73
lines changed

7 files changed

+392
-73
lines changed

editor/app/(api)/(public)/v1/west/t/invite/route.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { service_role } from "@/lib/supabase/server";
2+
import { buildTenantSiteBaseUrl } from "@/lib/tenant-url";
23
import { headers } from "next/headers";
34
import { NextResponse, type NextRequest } from "next/server";
45
import { Platform } from "@/lib/platform";
@@ -49,21 +50,24 @@ export async function POST(req: NextRequest) {
4950
return NextResponse.json({ error: mint_err }, { status: 500 });
5051
}
5152

52-
const { www_name, www_route_path } = invitation.campaign;
53-
const baseUrl = new URL(
54-
www_route_path ?? "",
55-
IS_HOSTED
56-
? `https://${www_name}.grida.site/`
57-
: `http://${www_name}.localhost:3000/`
58-
);
53+
const { www_name: raw_www_name, www_route_path } = invitation.campaign;
54+
assert(raw_www_name, "campaign.www_name is required");
55+
56+
const baseUrl = await buildTenantSiteBaseUrl({
57+
www_name: raw_www_name,
58+
www_route_path,
59+
hosted: IS_HOSTED,
60+
prefer_canonical: true,
61+
});
62+
const inviteUrl = `${baseUrl}/t/${invitation.code}`;
5963

6064
return NextResponse.json({
6165
data: {
6266
code: invitation.code,
6367
sharable: {
6468
referrer_name: invitation.referrer.referrer_name ?? "",
6569
invitation_code: invitation.code,
66-
url: `${baseUrl.toString()}/t/${invitation.code}`,
70+
url: inviteUrl,
6771
} satisfies Platform.WEST.Referral.SharableContext,
6872
},
6973
error: null,

editor/app/(api)/(public)/v1/west/t/refresh/route.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { service_role } from "@/lib/supabase/server";
2+
import { buildTenantSiteBaseUrl } from "@/lib/tenant-url";
23
import { headers } from "next/headers";
34
import { NextResponse, type NextRequest } from "next/server";
45
import { Platform } from "@/lib/platform";
@@ -56,22 +57,24 @@ export async function POST(req: NextRequest) {
5657
return NextResponse.json({ error: refresh_err }, { status: 400 });
5758
}
5859

59-
const { www_name, www_route_path } = invitation.campaign;
60+
const { www_name: raw_www_name, www_route_path } = invitation.campaign;
61+
assert(raw_www_name, "campaign.www_name is required");
6062

61-
const baseUrl = new URL(
62-
www_route_path ?? "",
63-
IS_HOSTED
64-
? `https://${www_name}.grida.site/`
65-
: `http://${www_name}.localhost:3000/`
66-
);
63+
const baseUrl = await buildTenantSiteBaseUrl({
64+
www_name: raw_www_name,
65+
www_route_path,
66+
hosted: IS_HOSTED,
67+
prefer_canonical: true,
68+
});
69+
const inviteUrl = `${baseUrl}/t/${invitation.code}`;
6770

6871
return NextResponse.json({
6972
data: {
7073
code: invitation.code,
7174
sharable: {
7275
referrer_name: invitation.referrer.referrer_name ?? "",
7376
invitation_code: invitation.code,
74-
url: `${baseUrl.toString()}/t/${invitation.code}`,
77+
url: inviteUrl,
7578
} satisfies Platform.WEST.Referral.SharableContext,
7679
},
7780
error: null,

editor/grida-forms-hosted/e/formview.tsx

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ import { useValue } from "@/lib/spock";
5757
import { Spinner } from "@/components/ui/spinner";
5858
import { PhoneFieldDefaultCountryProvider } from "@/components/formfield/phone-field";
5959
import type { FormAgentGeo } from "@/grida-forms/formstate/core/geo";
60+
import resources from "@/i18n";
61+
import { select_lang } from "@/i18n/utils";
62+
import { supported_form_page_languages } from "@/k/supported_languages";
6063

6164
const html_form_id = "form";
6265

@@ -94,25 +97,46 @@ export interface FormViewTranslation {
9497
};
9598
}
9699

97-
const default_form_view_translation_en: FormViewTranslation = {
98-
next: "Next",
99-
back: "Back",
100-
submit: "Submit",
101-
pay: "Pay",
102-
email_challenge: {
103-
verify: "Verify",
104-
sending: "Sending",
105-
verify_code: "Verify",
106-
enter_verification_code: "Enter verification code",
107-
code_sent: "A verification code has been sent to your inbox.",
108-
didnt_receive_code: "Didn't receive a code?",
109-
resend: "Resend",
110-
retry: "Retry",
111-
code_expired: "Verification code has expired.",
112-
incorrect_code: "Incorrect verification code. Please try again.",
113-
error_occurred: "An error occurred. Please try again later.",
114-
},
115-
};
100+
/**
101+
* Build a {@link FormViewTranslation} from shared i18n resources for the
102+
* given language code. Unknown / unsupported codes fall back to `"en"`.
103+
*/
104+
function build_form_view_translation(lang: string): FormViewTranslation {
105+
const lng = select_lang(lang, supported_form_page_languages, "en");
106+
const t = resources[lng].translation;
107+
const ec = t.email_challenge;
108+
109+
return {
110+
next: t.next,
111+
back: t.back,
112+
submit: t.submit,
113+
pay: t.pay,
114+
email_challenge: {
115+
verify: t.verify,
116+
sending: t.sending,
117+
verify_code: ec.verify_code,
118+
enter_verification_code: ec.enter_verification_code,
119+
code_sent: ec.code_sent,
120+
didnt_receive_code: ec.didnt_receive_code,
121+
resend: t.resend,
122+
retry: t.retry,
123+
code_expired: ec.code_expired,
124+
incorrect_code: ec.incorrect_code,
125+
error_occurred: ec.error_occurred,
126+
},
127+
};
128+
}
129+
130+
/** Canonical English translation, derived from the shared i18n resources. */
131+
const default_form_view_translation_en = build_form_view_translation("en");
132+
133+
const FormViewTranslationContext = React.createContext<FormViewTranslation>(
134+
default_form_view_translation_en
135+
);
136+
137+
function useFormViewTranslation() {
138+
return React.useContext(FormViewTranslationContext);
139+
}
116140

117141
type FormViewRootProps = {
118142
form_id: string;
@@ -122,6 +146,21 @@ type FormViewRootProps = {
122146
blocks: ClientRenderBlock[];
123147
tree: FormBlockTree<ClientRenderBlock[]>;
124148
defaultValues?: { [key: string]: string };
149+
/**
150+
* Optional language code (e.g. `"ko"`, `"en"`).
151+
* Resolves a {@link FormViewTranslation} from the shared i18n resources and
152+
* provides it via context so that `FormView.Body`, `FormView.Prev`,
153+
* `FormView.Next`, and `FormView.Submit` are automatically localised.
154+
*
155+
* An explicit {@link translation} prop takes precedence over `lang`.
156+
*/
157+
lang?: string;
158+
/**
159+
* Explicit {@link FormViewTranslation} object.
160+
* Takes precedence over {@link lang}. When both are omitted the context
161+
* defaults to English.
162+
*/
163+
translation?: FormViewTranslation;
125164
};
126165

127166
export function GridaFormsFormView(
@@ -143,7 +182,6 @@ export function GridaFormsFormView(
143182
>
144183
<GridaFormBody {...props} />
145184
<GridaFormFooter
146-
translation={props.translation}
147185
is_powered_by_branding_enabled={
148186
props.config.is_powered_by_branding_enabled
149187
}
@@ -155,9 +193,24 @@ export function GridaFormsFormView(
155193

156194
export function FormViewRoot({
157195
children,
196+
lang,
197+
translation: translationProp,
158198
...props
159199
}: React.PropsWithChildren<FormViewRootProps>) {
160-
return <Providers {...props}>{children}</Providers>;
200+
const translation = useMemo(
201+
() =>
202+
translationProp ??
203+
(lang
204+
? build_form_view_translation(lang)
205+
: default_form_view_translation_en),
206+
[translationProp, lang]
207+
);
208+
209+
return (
210+
<FormViewTranslationContext.Provider value={translation}>
211+
<Providers {...props}>{children}</Providers>
212+
</FormViewTranslationContext.Provider>
213+
);
161214
}
162215

163216
function Providers({
@@ -236,11 +289,13 @@ export function FormBody({
236289
onSubmit,
237290
onAfterSubmit,
238291
className,
239-
translation = default_form_view_translation_en,
292+
translation: translationProp,
240293
config,
241294
stylesheet,
242295
...formattributes
243296
}: FormBodyProps & HtmlFormElementProps & IOnSubmit) {
297+
const contextTranslation = useFormViewTranslation();
298+
const translation = translationProp ?? contextTranslation;
244299
const [state, dispatch] = useFormAgentState();
245300
const { tree, session_id, current_section_id, submit_hidden, onNext } =
246301
useFormAgent();
@@ -322,16 +377,14 @@ export function FormBody({
322377

323378
function GridaFormFooter({
324379
is_powered_by_branding_enabled,
325-
translation = default_form_view_translation_en,
326380
}: {
327381
is_powered_by_branding_enabled: boolean;
328-
translation?: FormViewTranslation;
329382
}) {
330383
const { pay_hidden } = useFormAgent();
331384

332385
return (
333386
<>
334-
<Footer shouldHidePay={pay_hidden} translation={translation} />
387+
<Footer shouldHidePay={pay_hidden} />
335388
{/* on desktop, branding attribute is below footer */}
336389
{is_powered_by_branding_enabled && (
337390
<div className="hidden md:block">
@@ -342,13 +395,9 @@ function GridaFormFooter({
342395
);
343396
}
344397

345-
function Footer({
346-
translation,
347-
shouldHidePay,
348-
}: {
349-
shouldHidePay: boolean;
350-
translation: FormViewTranslation;
351-
}) {
398+
function Footer({ shouldHidePay }: { shouldHidePay: boolean }) {
399+
const translation = useFormViewTranslation();
400+
352401
return (
353402
<footer
354403
className={cn(
@@ -358,9 +407,9 @@ function Footer({
358407
"md:static md:justify-start md:bg-transparent md:dark:bg-transparent"
359408
)}
360409
>
361-
<FormPrev>{translation.back}</FormPrev>
362-
<FormNext className="flex-1 md:w-auto">{translation.next}</FormNext>
363-
<FormSubmit className="flex-1 md:w-auto">{translation.submit}</FormSubmit>
410+
<FormPrev />
411+
<FormNext className="flex-1 md:w-auto" />
412+
<FormSubmit className="flex-1 md:w-auto" />
364413
<TossPaymentsPayButton
365414
data-pay-hidden={shouldHidePay}
366415
className={cn(
@@ -382,6 +431,7 @@ function FormPrev({
382431
className?: string;
383432
}>) {
384433
const { has_previous, onPrevious } = useFormAgent();
434+
const translation = useFormViewTranslation();
385435

386436
return (
387437
<Button
@@ -390,7 +440,7 @@ function FormPrev({
390440
className={cn("data-[next-hidden='true']:hidden", className)}
391441
onClick={onPrevious}
392442
>
393-
{children}
443+
{children ?? translation.back}
394444
</Button>
395445
);
396446
}
@@ -402,6 +452,7 @@ function FormNext({
402452
className?: string;
403453
}>) {
404454
const { has_next } = useFormAgent();
455+
const translation = useFormViewTranslation();
405456

406457
return (
407458
<Button
@@ -411,7 +462,7 @@ function FormNext({
411462
type="submit"
412463
className={cn("data-[next-hidden='true']:hidden", className)}
413464
>
414-
{children}
465+
{children ?? translation.next}
415466
</Button>
416467
);
417468
}
@@ -423,6 +474,7 @@ function FormSubmit({
423474
className?: string;
424475
}>) {
425476
const { submit_hidden, is_submitting } = useFormAgent();
477+
const translation = useFormViewTranslation();
426478

427479
return (
428480
<Button
@@ -442,7 +494,7 @@ function FormSubmit({
442494
<Spinner className="me-2" />
443495
</div>
444496
)}
445-
{children}
497+
{children ?? translation.submit}
446498
</Button>
447499
);
448500
}

editor/i18n/resources.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,18 @@ export interface Translation {
2323
submit: string;
2424
pay: string;
2525
home: string;
26-
// Common UI labels (forms)
27-
verify?: string;
28-
resend?: string;
29-
retry?: string;
30-
sending?: string;
31-
email_challenge?: {
32-
verify_code?: string;
33-
enter_verification_code?: string;
34-
code_sent?: string;
35-
didnt_receive_code?: string;
36-
code_expired?: string;
37-
incorrect_code?: string;
38-
error_occurred?: string;
26+
verify: string;
27+
resend: string;
28+
retry: string;
29+
sending: string;
30+
email_challenge: {
31+
verify_code: string;
32+
enter_verification_code: string;
33+
code_sent: string;
34+
didnt_receive_code: string;
35+
code_expired: string;
36+
incorrect_code: string;
37+
error_occurred: string;
3938
};
4039
left_in_stock: string;
4140
sold_out: string;

0 commit comments

Comments
 (0)