(() =>
parseOrgPickerEmbedOptions(location.search),
);
- const pickerSrc = buildOrgPickerEmbedSrc(options);
+ const pickerSrcBase = buildOrgPickerEmbedSrc(options);
+ const pickerSrc = shareToken
+ ? `${pickerSrcBase}&token=${encodeURIComponent(shareToken)}`
+ : pickerSrcBase;
return (
diff --git a/orgfront/tests/orgchart-picker.spec.ts b/orgfront/tests/orgchart-picker.spec.ts
index f55056db..7fbf98ae 100644
--- a/orgfront/tests/orgchart-picker.spec.ts
+++ b/orgfront/tests/orgchart-picker.spec.ts
@@ -1,5 +1,13 @@
import { expect, test } from "@playwright/test";
+const shareToken = "playwright";
+
+function withShareToken(path: string) {
+ return path.includes("?")
+ ? `${path}&token=${shareToken}`
+ : `${path}?token=${shareToken}`;
+}
+
type TenantFixture = {
id: string;
type: string;
@@ -75,41 +83,58 @@ async function seedOrgfrontAuth(page: Parameters[0]["page"]) {
},
expires_at: issuedAt + 3600,
};
-
- window.localStorage.setItem(
+ const storageKeys = [
+ "user:http://localhost:5000/oidc:orgfront",
+ "user:http://localhost:5000/oidc/:orgfront",
+ "user:http://localhost:5000/oidc:devfront",
+ "user:http://localhost:5000/oidc/:devfront",
+ "user:http://172.16.9.189:5000/oidc:orgfront",
+ "user:http://172.16.9.189:5000/oidc/:orgfront",
"oidc.user:http://localhost:5000/oidc:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc:devfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:devfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://172.16.9.189:5000/oidc:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://172.16.9.189:5000/oidc/:orgfront",
- JSON.stringify(mockOidcUser),
- );
+ ];
+ for (const key of storageKeys) {
+ window.localStorage.setItem(key, JSON.stringify(mockOidcUser));
+ }
+ window.localStorage.setItem("playwright_auth_bypass", "1");
window.localStorage.setItem("dev_tenant_id", "group-hmac");
},
{ issuedAt: nowInSeconds },
);
await page.route("**/oidc/**", async (route) => {
+ const url = route.request().url();
+ if (url.includes(".well-known/openid-configuration")) {
+ await route.fulfill({
+ json: {
+ issuer: "http://localhost:5000/oidc",
+ authorization_endpoint: "http://localhost:5000/oidc/auth",
+ token_endpoint: "http://localhost:5000/oidc/token",
+ jwks_uri: "http://localhost:5000/oidc/jwks",
+ userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
+ end_session_endpoint: "http://localhost:5000/oidc/session/end",
+ },
+ headers: { "Access-Control-Allow-Origin": "*" },
+ });
+ return;
+ }
+
+ if (url.includes("/jwks")) {
+ await route.fulfill({
+ json: { keys: [] },
+ headers: { "Access-Control-Allow-Origin": "*" },
+ });
+ return;
+ }
+
await route.fulfill({
status: 200,
- contentType: "application/json",
- body: JSON.stringify({ keys: [] }),
+ body: "ok",
+ headers: { "Access-Control-Allow-Origin": "*" },
});
});
}
@@ -186,7 +211,7 @@ test.beforeEach(async ({ page }) => {
test("developer navigation exposes chart, picker, and embed preview", async ({
page,
}) => {
- await page.goto("/");
+ await page.goto(withShareToken("/chart"));
await expect(page.getByRole("link", { name: "조직도" })).toBeVisible();
await expect(page.getByRole("link", { name: "조직 선택기" })).toBeVisible();
@@ -207,7 +232,7 @@ test("developer navigation exposes chart, picker, and embed preview", async ({
test("picker menu lets developers switch selection mode and selectable type", async ({
page,
}) => {
- await page.goto("/picker");
+ await page.goto(withShareToken("/picker"));
await expect(page.getByLabel("선택 모드")).toHaveValue("multiple");
await expect(page.getByLabel("선택 대상")).toHaveValue("both");
@@ -230,7 +255,7 @@ test("picker menu lets developers switch selection mode and selectable type", as
test("picker displays user names with job title and position", async ({
page,
}) => {
- await page.goto("/embed/picker?mode=single&select=user");
+ await page.goto(withShareToken("/embed/picker?mode=single&select=user"));
await expect(
page.getByRole("button", {
@@ -242,7 +267,7 @@ test("picker displays user names with job title and position", async ({
test("embed preview menu updates the iframe picker source", async ({
page,
}) => {
- await page.goto("/embed-preview");
+ await page.goto(withShareToken("/embed-preview"));
await expect(page.getByLabel("선택 모드")).toHaveValue("multiple");
await expect(page.getByLabel("선택 대상")).toHaveValue("both");
@@ -297,7 +322,7 @@ test("embed preview menu updates the iframe picker source", async ({
test("embed preview passes tenant id and custom dimensions through the picker url", async ({
page,
}) => {
- await page.goto("/embed-preview");
+ await page.goto(withShareToken("/embed-preview"));
await page.getByLabel("tenant ID").fill("company-baron");
await page.getByLabel("임베딩 너비").fill("520");
@@ -325,7 +350,9 @@ test("embed preview passes tenant id and custom dimensions through the picker ur
test("embed picker scopes the tree by tenant id, hides users for tenant selection, and keeps direct members before child tenants", async ({
page,
}) => {
- await page.goto("/embed-preview?tenantId=company-baron&select=tenant");
+ await page.goto(
+ withShareToken("/embed-preview?tenantId=company-baron&select=tenant"),
+ );
await expect(page.getByLabel("tenant ID")).toHaveValue("company-baron");
await expect(page.getByTestId("embed-preview-src")).toContainText(
@@ -352,7 +379,7 @@ test("embed picker scopes the tree by tenant id, hides users for tenant selectio
test("embed picker keeps the lightweight search controls inside the picker section at the default embed width", async ({
page,
}) => {
- await page.goto("/embed-preview");
+ await page.goto(withShareToken("/embed-preview"));
const picker = page.frameLocator("iframe");
const searchSection = picker.getByTestId("org-picker-search-section");
@@ -379,7 +406,7 @@ test("embed picker keeps the lightweight search controls inside the picker secti
test("embed picker keeps only the lightweight picker surface scrollable", async ({
page,
}) => {
- await page.goto("/embed-preview");
+ await page.goto(withShareToken("/embed-preview"));
const picker = page.frameLocator("iframe");
await expect(
@@ -415,7 +442,7 @@ test("embed picker keeps only the lightweight picker surface scrollable", async
test("embed preview can hide the descendant selection switch", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=multiple&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
await expect(page.getByLabel("하위 선택 스위치 표시")).toBeChecked();
await page.getByLabel("하위 선택 스위치 표시").uncheck();
@@ -434,7 +461,7 @@ test("embed preview can hide the descendant selection switch", async ({
test("embed picker renders compact tree rows with member emails", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=single&select=user");
+ await page.goto(withShareToken("/embed-preview?mode=single&select=user"));
const picker = page.frameLocator("iframe");
await expect(picker.getByText("user-eng@example.com")).toBeVisible();
@@ -451,7 +478,7 @@ test("embed picker renders compact tree rows with member emails", async ({
test("embed picker filters organizations and users by id, name, and metadata", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=multiple&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
const picker = page.frameLocator("iframe");
const search = picker.getByLabel("조직/구성원 검색");
@@ -475,7 +502,7 @@ test("embed picker filters organizations and users by id, name, and metadata", a
test("embed picker search does not keep unmatched descendants under a matching organization", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=multiple&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
const picker = page.frameLocator("iframe");
await picker.getByLabel("조직/구성원 검색").fill("센");
@@ -489,7 +516,7 @@ test("embed picker search does not keep unmatched descendants under a matching o
test("embed picker posts a single user selection with type, id, and name", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=single&select=user");
+ await page.goto(withShareToken("/embed-preview?mode=single&select=user"));
const picker = page.frameLocator("iframe");
await picker
@@ -507,7 +534,7 @@ test("embed picker posts a single user selection with type, id, and name", async
test("embed picker single selection counts only the selected node without descendants", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=single&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
await picker
@@ -528,7 +555,7 @@ test("embed picker single selection counts only the selected node without descen
test("embed picker highlights a single selected item without tree connectors", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=single&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
await expect(
@@ -548,7 +575,7 @@ test("embed picker highlights a single selected item without tree connectors", a
test("embed picker renders tenant names with the dedicated tenant text color", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=single&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=single&select=both"));
const picker = page.frameLocator("iframe");
const tenantName = picker.getByTestId("org-picker-node-name-tenant").first();
@@ -563,7 +590,7 @@ test("embed picker renders tenant names with the dedicated tenant text color", a
test("embed picker includes descendants by default and can disable descendant inclusion", async ({
page,
}) => {
- await page.goto("/embed-preview?mode=multiple&select=both");
+ await page.goto(withShareToken("/embed-preview?mode=multiple&select=both"));
let picker = page.frameLocator("iframe");
await expect(
@@ -582,7 +609,9 @@ test("embed picker includes descendants by default and can disable descendant in
await expect(output).toContainText('"id": "user-platform"');
await page.goto(
- "/embed-preview?mode=multiple&select=both&includeDescendants=false",
+ withShareToken(
+ "/embed-preview?mode=multiple&select=both&includeDescendants=false",
+ ),
);
picker = page.frameLocator("iframe");
await picker.getByLabel("Engineering 선택").check();
diff --git a/orgfront/tests/orgchart-vector-render.spec.ts b/orgfront/tests/orgchart-vector-render.spec.ts
index 7948a492..bfd55ae0 100644
--- a/orgfront/tests/orgchart-vector-render.spec.ts
+++ b/orgfront/tests/orgchart-vector-render.spec.ts
@@ -214,41 +214,58 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
},
expires_at: seededIssuedAt + 3600,
};
-
- window.localStorage.setItem(
+ const storageKeys = [
+ "user:http://localhost:5000/oidc:orgfront",
+ "user:http://localhost:5000/oidc/:orgfront",
+ "user:http://localhost:5000/oidc:devfront",
+ "user:http://localhost:5000/oidc/:devfront",
+ "user:http://172.16.9.189:5000/oidc:orgfront",
+ "user:http://172.16.9.189:5000/oidc/:orgfront",
"oidc.user:http://localhost:5000/oidc:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc:devfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:devfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://172.16.9.189:5000/oidc:orgfront",
- JSON.stringify(mockOidcUser),
- );
- window.localStorage.setItem(
"oidc.user:http://172.16.9.189:5000/oidc/:orgfront",
- JSON.stringify(mockOidcUser),
- );
+ ];
+ for (const key of storageKeys) {
+ window.localStorage.setItem(key, JSON.stringify(mockOidcUser));
+ }
+ window.localStorage.setItem("playwright_auth_bypass", "1");
window.localStorage.setItem("dev_tenant_id", "group");
},
{ issuedAt },
);
await page.route("**/oidc/**", async (route) => {
+ const url = route.request().url();
+ if (url.includes(".well-known/openid-configuration")) {
+ await route.fulfill({
+ json: {
+ issuer: "http://localhost:5000/oidc",
+ authorization_endpoint: "http://localhost:5000/oidc/auth",
+ token_endpoint: "http://localhost:5000/oidc/token",
+ jwks_uri: "http://localhost:5000/oidc/jwks",
+ userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
+ end_session_endpoint: "http://localhost:5000/oidc/session/end",
+ },
+ headers: { "Access-Control-Allow-Origin": "*" },
+ });
+ return;
+ }
+
+ if (url.includes("/jwks")) {
+ await route.fulfill({
+ json: { keys: [] },
+ headers: { "Access-Control-Allow-Origin": "*" },
+ });
+ return;
+ }
+
await route.fulfill({
status: 200,
- contentType: "application/json",
- body: JSON.stringify({ keys: [] }),
+ body: "ok",
+ headers: { "Access-Control-Allow-Origin": "*" },
});
});
diff --git a/orgfront/tests/orgfront-auto-login.spec.ts b/orgfront/tests/orgfront-auto-login.spec.ts
index 3fda2cb6..e15b4eb5 100644
--- a/orgfront/tests/orgfront-auto-login.spec.ts
+++ b/orgfront/tests/orgfront-auto-login.spec.ts
@@ -56,7 +56,7 @@ test("orgfront login auto parameter starts OIDC authorization", async ({
const parsed = new URL(oidc.authorizationURL());
expect(parsed.searchParams.get("client_id")).toBe("orgfront");
expect(parsed.searchParams.get("redirect_uri")).toBe(
- "http://localhost:5175/auth/callback",
+ "http://127.0.0.1:4175/auth/callback",
);
expect(parsed.searchParams.get("response_type")).toBe("code");
expect(parsed.searchParams.get("scope") ?? "").toContain("openid");
diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml
index 5b81e861..4393a094 100644
--- a/userfront/assets/translations/en.toml
+++ b/userfront/assets/translations/en.toml
@@ -15,6 +15,7 @@ saman = "Saman"
[domain.tenant_type]
company = "Company"
company_group = "Company Group"
+organization = "Organization"
personal = "Personal"
user_group = "User Group"
diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml
index e33608f5..8a84d53f 100644
--- a/userfront/assets/translations/ko.toml
+++ b/userfront/assets/translations/ko.toml
@@ -15,6 +15,7 @@ saman = "삼안"
[domain.tenant_type]
company = "COMPANY (일반 기업)"
company_group = "COMPANY_GROUP (그룹사/지주사)"
+organization = "ORGANIZATION (정규 조직)"
personal = "PERSONAL (개인 워크스페이스)"
user_group = "USER_GROUP (내부 부서/팀)"
diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml
index f48cb117..497bb8c6 100644
--- a/userfront/assets/translations/template.toml
+++ b/userfront/assets/translations/template.toml
@@ -15,6 +15,7 @@ saman = ""
[domain.tenant_type]
company = ""
company_group = ""
+organization = ""
personal = ""
user_group = ""
diff --git a/userfront/lib/core/constants/error_whitelist.dart b/userfront/lib/core/constants/error_whitelist.dart
index af4e8558..c7f90188 100644
--- a/userfront/lib/core/constants/error_whitelist.dart
+++ b/userfront/lib/core/constants/error_whitelist.dart
@@ -1,14 +1,16 @@
-const Map internalErrorWhitelistMessages = {
- 'settings_disabled': '현재 계정 설정 화면은 준비 중입니다.',
- 'invalid_session': '세션이 만료되었습니다. 다시 로그인해 주세요.',
- 'verification_required': '추가 인증이 필요합니다. 안내에 따라 진행해 주세요.',
- 'recovery_expired': '재설정 링크가 만료되었습니다. 다시 요청해 주세요.',
- 'recovery_invalid': '재설정 링크가 유효하지 않습니다.',
- 'rate_limited': '요청이 많습니다. 잠시 후 다시 시도해 주세요.',
- 'not_found': '요청한 페이지를 찾을 수 없습니다.',
- 'bad_request': '입력값을 확인해 주세요.',
- 'password_or_email_mismatch': '이메일 혹은 비밀번호가 일치하지 않습니다.',
- 'tenant_not_allowed': '허용되지 않은 테넌트입니다.',
+const Map internalErrorWhitelistMessageKeys = {
+ 'settings_disabled': 'msg.userfront.error.whitelist.settings_disabled',
+ 'invalid_session': 'msg.userfront.error.whitelist.invalid_session',
+ 'verification_required':
+ 'msg.userfront.error.whitelist.verification_required',
+ 'recovery_expired': 'msg.userfront.error.whitelist.recovery_expired',
+ 'recovery_invalid': 'msg.userfront.error.whitelist.recovery_invalid',
+ 'rate_limited': 'msg.userfront.error.whitelist.rate_limited',
+ 'not_found': 'msg.userfront.error.whitelist.not_found',
+ 'bad_request': 'msg.userfront.error.whitelist.bad_request',
+ 'password_or_email_mismatch':
+ 'msg.userfront.error.whitelist.password_or_email_mismatch',
+ 'tenant_not_allowed': 'msg.userfront.error.whitelist.tenant_not_allowed',
};
const Set oryBypassErrorCodes = {
diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart
index 651ddd23..8d455e11 100644
--- a/userfront/lib/core/services/auth_proxy_service.dart
+++ b/userfront/lib/core/services/auth_proxy_service.dart
@@ -65,7 +65,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.password_policy_fetch',
- '비밀번호 정책을 불러오지 못했습니다.',
+ 'Failed to load the password policy.',
);
}
}
@@ -84,7 +84,7 @@ class AuthProxyService {
}
throw _error(
'err.userfront.auth_proxy.profile_load',
- '프로필을 불러오지 못했습니다: {{error}}',
+ 'Failed to load the profile: {{error}}',
detail: response.body,
);
} finally {
@@ -110,7 +110,7 @@ class AuthProxyService {
}
throw _error(
'err.userfront.auth_proxy.profile_load',
- '프로필을 불러오지 못했습니다: {{error}}',
+ 'Failed to load the profile: {{error}}',
detail: response.body,
);
} finally {
@@ -144,7 +144,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.tenant_info_fetch',
- '테넌트 정보를 불러오지 못했습니다.',
+ 'Failed to load tenant information.',
);
}
}
@@ -180,7 +180,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.login_init',
- '로그인 초기화에 실패했습니다: {{error}}',
+ 'Failed to initialize login: {{error}}',
detail: response.body,
);
}
@@ -205,7 +205,7 @@ class AuthProxyService {
}
throw _error(
'err.userfront.auth_proxy.login_poll',
- '로그인 상태 확인에 실패했습니다: {{error}}',
+ 'Failed to check login status: {{error}}',
detail: response.body,
);
}
@@ -227,7 +227,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.verify_failed',
- '검증에 실패했습니다: {{error}}',
+ 'Verification failed: {{error}}',
detail: response.body,
);
}
@@ -261,7 +261,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.verify_failed',
- '검증에 실패했습니다: {{error}}',
+ 'Verification failed: {{error}}',
detail: response.body,
);
}
@@ -281,7 +281,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.revoke',
- '세션 종료에 실패했습니다: {{error}}',
+ 'Failed to revoke the session: {{error}}',
detail: response.body,
);
}
@@ -304,7 +304,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.load',
- '활성 세션을 불러오지 못했습니다: {{error}}',
+ 'Failed to load the active sessions: {{error}}',
detail: response.body,
);
}
@@ -342,7 +342,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.verify_failed',
- '검증에 실패했습니다: {{error}}',
+ 'Verification failed: {{error}}',
detail: response.body,
);
}
@@ -568,7 +568,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.sms_send',
- 'SMS 전송에 실패했습니다: {{error}}',
+ 'Failed to send SMS: {{error}}',
detail: response.body,
);
}
@@ -591,7 +591,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.code_verify',
- '인증 코드 확인에 실패했습니다: {{error}}',
+ 'Failed to verify the code: {{error}}',
detail: response.body,
);
}
@@ -609,7 +609,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.qr_init',
- 'QR 로그인을 시작하지 못했습니다: {{error}}',
+ 'Failed to start QR login: {{error}}',
detail: response.body,
);
}
@@ -631,7 +631,7 @@ class AuthProxyService {
}
throw _error(
'err.userfront.auth_proxy.qr_poll',
- 'QR 상태 확인에 실패했습니다: {{error}}',
+ 'Failed to check QR status: {{error}}',
detail: response.body,
);
}
@@ -669,7 +669,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.qr_approve',
- 'QR 승인에 실패했습니다: {{error}}',
+ 'Failed to approve QR login: {{error}}',
detail: response.body,
);
}
@@ -720,7 +720,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.user_create',
- '사용자 생성에 실패했습니다: {{error}}',
+ 'Failed to create the user: {{error}}',
detail: response.body,
);
}
@@ -749,7 +749,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.user_list',
- '사용자 목록 조회에 실패했습니다: {{error}}',
+ 'Failed to load the user list: {{error}}',
detail: response.body,
);
}
@@ -770,7 +770,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.user_delete',
- '사용자 삭제에 실패했습니다: {{error}}',
+ 'Failed to delete the user: {{error}}',
detail: response.body,
);
}
@@ -796,7 +796,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.user_status_update',
- '상태 업데이트에 실패했습니다: {{error}}',
+ 'Failed to update the user status: {{error}}',
detail: response.body,
);
}
@@ -829,7 +829,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.user_update',
- '사용자 수정에 실패했습니다: {{error}}',
+ 'Failed to update the user: {{error}}',
detail: response.body,
);
}
@@ -855,7 +855,7 @@ class AuthProxyService {
} else {
throw _error(
'err.userfront.auth_proxy.linked_apps_load',
- '연동된 앱 목록을 불러오지 못했습니다.',
+ 'Failed to load linked applications.',
);
}
} finally {
@@ -1043,7 +1043,7 @@ class AuthProxyService {
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.phone_code_send',
- '인증 코드 전송에 실패했습니다: {{error}}',
+ 'Failed to send the verification code: {{error}}',
detail: response.body,
);
}
diff --git a/userfront/lib/features/auth/presentation/error_screen.dart b/userfront/lib/features/auth/presentation/error_screen.dart
index 016d3fe4..f1dc9d81 100644
--- a/userfront/lib/features/auth/presentation/error_screen.dart
+++ b/userfront/lib/features/auth/presentation/error_screen.dart
@@ -282,9 +282,9 @@ class _ErrorScreenState extends State {
final isProd = widget.isProdOverride ?? AuthProxyService.isProdEnv;
final normalizedCode = (widget.errorCode ?? '').trim();
final hasCode = normalizedCode.isNotEmpty;
- final internalWhitelistFallback =
- internalErrorWhitelistMessages[normalizedCode];
- final isInternalWhitelisted = internalWhitelistFallback != null;
+ final internalWhitelistKey =
+ internalErrorWhitelistMessageKeys[normalizedCode];
+ final isInternalWhitelisted = internalWhitelistKey != null;
final isOryBypass = hasCode && oryBypassErrorCodes.contains(normalizedCode);
final isKnownProdCode = hasCode && (isInternalWhitelisted || isOryBypass);
final isTenantAccessBlocked = normalizedCode == 'tenant_not_allowed';
@@ -294,7 +294,7 @@ class _ErrorScreenState extends State {
final title = isTenantAccessBlocked
? tr(
'msg.userfront.error.tenant.page_title',
- fallback: '애플리케이션 접근이 제한되었습니다',
+ fallback: 'Application access is restricted',
)
: isProd
? tr('msg.userfront.error.title')
@@ -332,17 +332,18 @@ class _ErrorScreenState extends State {
final showTenantLookupFallback =
_tenantAccessDetails == null &&
(emailLabel.isEmpty || tenantLabel.isEmpty);
+ final internalWhitelistDetail = internalWhitelistKey == null
+ ? null
+ : tr(internalWhitelistKey);
final detail = isTenantAccessBlocked
? tr(
'msg.userfront.error.tenant.detail',
- fallback: '현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다.',
+ fallback:
+ 'The current signed-in account cannot access this application.',
)
: isProd
? (isInternalWhitelisted
- ? tr(
- 'msg.userfront.error.whitelist.$normalizedCode',
- fallback: internalWhitelistFallback,
- )
+ ? internalWhitelistDetail!
: (isOryBypass
? tr(
'msg.userfront.error.ory.$normalizedCode',
@@ -422,7 +423,7 @@ class _ErrorScreenState extends State {
Text(
tr(
'msg.userfront.error.tenant.title',
- fallback: '접근 제한 정보',
+ fallback: 'Access restriction details',
),
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
@@ -447,7 +448,8 @@ class _ErrorScreenState extends State {
child: Text(
tr(
'msg.userfront.error.tenant.loading',
- fallback: '현재 계정 정보를 불러오는 중입니다.',
+ fallback:
+ 'Loading the current account details.',
),
style: theme.textTheme.bodySmall
?.copyWith(
@@ -462,39 +464,39 @@ class _ErrorScreenState extends State {
_InfoRow(
label: tr(
'msg.userfront.error.tenant.account',
- fallback: '계정',
+ fallback: 'Account',
),
value: emailLabel.isNotEmpty
? emailLabel
: tr(
'msg.userfront.error.tenant.account_unknown',
- fallback: '알 수 없음',
+ fallback: 'Unknown',
),
),
const SizedBox(height: 8),
_InfoRow(
label: tr(
'msg.userfront.error.tenant.primary_tenant',
- fallback: '대표 소속 테넌트',
+ fallback: 'Primary affiliated tenant',
),
value: tenantLabel.isNotEmpty
? tenantLabel
: tr(
'msg.userfront.error.tenant.tenant_unknown',
- fallback: '알 수 없음',
+ fallback: 'Unknown',
),
),
const SizedBox(height: 8),
_InfoRow(
label: tr(
'msg.userfront.error.tenant.affiliated_tenants',
- fallback: '전체 소속 테넌트',
+ fallback: 'All affiliated tenants',
),
value: affiliatedTenantLabels.isNotEmpty
? affiliatedTenantLabels.join(', ')
: tr(
'msg.userfront.error.tenant.tenant_unknown',
- fallback: '알 수 없음',
+ fallback: 'Unknown',
),
),
if (showTenantLookupFallback) ...[
@@ -503,7 +505,7 @@ class _ErrorScreenState extends State {
tr(
'msg.userfront.error.tenant.lookup_fallback',
fallback:
- '표시 정보가 충분하지 않아 일부 항목은 확인되지 않을 수 있습니다.',
+ 'Some fields may be unavailable because there is not enough profile information to display.',
),
style: theme.textTheme.bodySmall
?.copyWith(
@@ -518,7 +520,7 @@ class _ErrorScreenState extends State {
tr(
'msg.userfront.error.tenant.load_failed',
fallback:
- '계정 정보를 확인하지 못했습니다. 다시 시도해 주세요.',
+ 'Failed to load account details. Please try again.',
),
style: theme.textTheme.bodySmall
?.copyWith(
@@ -548,7 +550,7 @@ class _ErrorScreenState extends State {
Text(
tr(
'msg.userfront.error.tenant.allowed_box_title',
- fallback: '접속 가능 테넌트',
+ fallback: 'Allowed tenants',
),
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
@@ -559,7 +561,7 @@ class _ErrorScreenState extends State {
_InfoRow(
label: tr(
'msg.userfront.error.tenant.allowed_tenants',
- fallback: '접속 가능 테넌트',
+ fallback: 'Allowed tenants',
),
value: allowedTenantLabels.join(', '),
),
@@ -567,11 +569,11 @@ class _ErrorScreenState extends State {
_InfoRow(
label: tr(
'msg.userfront.error.tenant.allowed_tenants',
- fallback: '접속 가능 테넌트',
+ fallback: 'Allowed tenants',
),
value: tr(
'msg.userfront.error.tenant.tenant_unknown',
- fallback: '알 수 없음',
+ fallback: 'Unknown',
),
),
],
diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart
index 8cf6946c..66c607ef 100644
--- a/userfront/lib/features/auth/presentation/signup_screen.dart
+++ b/userfront/lib/features/auth/presentation/signup_screen.dart
@@ -838,11 +838,18 @@ class _SignupScreenState extends State {
static String _resolveAgreementText(
String key, {
required String fallback,
+ String? englishFallback,
required Set placeholders,
}) {
final localized = tr(key, fallback: '').trim();
- if (localized.isEmpty || placeholders.contains(localized)) {
- return fallback;
+ final hasCorruptedEscapes = RegExp(r'\\{3,}').hasMatch(localized);
+ final preferredLocaleCode = resolvePreferredLocaleCode();
+ final useEnglishFallback =
+ preferredLocaleCode.startsWith('en') && englishFallback != null;
+ if (localized.isEmpty ||
+ placeholders.contains(localized) ||
+ hasCorruptedEscapes) {
+ return useEnglishFallback ? englishFallback : fallback;
}
return localized;
}
@@ -918,10 +925,106 @@ class _SignupScreenState extends State {
본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.
부칙
본 약관은 2024년 10월 1일부터 시행됩니다.
+""";
+ const englishFallback = """
+Baron Software Terms of Service
+
+Chapter 1. General Provisions
+Article 1 (Purpose)
+These Terms of Service define the rights, obligations, responsibilities, and other necessary matters between Baron Consultant Co., Ltd. (the "Company") and users in connection with the use of Baron Software and related services (the "Service").
+
+Article 2 (Definitions)
+1. "Service" means the software and related services provided by the Company.
+2. "User" means any member or non-member who accesses and uses the Service.
+3. "Member" means a person who agrees to these Terms and enters into a service agreement with the Company.
+4. "Non-member" means a person who uses part of the Service without registering as a member.
+
+Article 3 (Effect and Amendment of the Terms)
+These Terms take effect when the User agrees to them and the Company accepts the registration. The Company may amend these Terms when necessary, and amended Terms become effective after notice is provided through the Service.
+
+Article 4 (Governing Rules)
+Matters not expressly provided in these Terms shall be governed by applicable laws of the Republic of Korea and general commercial practice.
+
+Chapter 2. Service Agreement
+Article 5 (Formation of the Agreement)
+The service agreement is formed when the User agrees to these Terms, submits the registration form provided by the Company, and the Company approves the registration.
+
+Article 6 (Reservation or Refusal of Registration)
+The Company may reserve or refuse registration if the application contains false information or if it is technically difficult to provide the Service.
+
+Article 7 (Changes to User Information)
+Members may review and edit their information at any time through the account management menu. Members must promptly update changed information and are responsible for problems arising from failure to do so.
+
+Chapter 3. Privacy Protection
+Article 8 (Principles of Privacy Protection)
+The Company protects Members' personal information in accordance with applicable laws. Detailed privacy matters are governed by the separate Privacy Policy.
+
+Article 9 (Compliance with the Privacy Policy)
+The collection, use, disclosure, retention, and protection of personal information are governed by the Privacy Policy, which Users may review at any time.
+
+Article 10 (Children Under 14)
+If the Company collects personal information from a child under the age of 14, the consent of a legal guardian is required.
+
+Chapter 4. Use of the Service
+Article 11 (Provision of the Service)
+The Company begins providing the Service once a registration request is approved. In principle, the Service is available 24 hours a day, 7 days a week.
+
+Article 12 (Change or Suspension of the Service)
+The Company may change or suspend the Service after prior notice when provision of the Service becomes difficult.
+
+Chapter 5. Information and Advertising
+Article 13 (Information and Advertising)
+The Company may provide information and advertising considered necessary during use of the Service. Members may opt out of unwanted communications where permitted.
+
+Chapter 6. User Content
+Article 14 (Management of Content)
+The Company may delete content posted by a Member if it is illegal or violates these Terms.
+
+Article 15 (Copyright)
+Copyright in content posted by Members belongs to the Member, but the Company may use such content for service promotion and improvement where permitted by law.
+
+Chapter 7. Termination and Restrictions
+Article 16 (Termination)
+Members may request termination of the agreement at any time, and the Company will process the request promptly.
+
+Article 17 (Restriction of Use)
+The Company may restrict access to the Service if a Member violates these Terms.
+
+Chapter 8. Damages and Disclaimer
+Article 18 (Damages)
+The Company is not liable for damages arising from free services unless required by law.
+
+Article 19 (Disclaimer)
+The Company is not liable where the Service cannot be provided due to force majeure such as natural disasters.
+
+Chapter 9. Paid Services
+Article 20 (Use of Paid Services)
+The Company may provide certain services for a fee. Pricing, payment methods, and refund procedures will be described on the service information page and payment screen. Fees are generally prepaid.
+
+Article 21 (Refund Policy)
+Users may receive a full refund if they do not start using a paid service within 7 days after payment. Partial refunds may apply when service suspension occurs for reasons not attributable to the User.
+
+Article 22 (Suspension and Cancellation of Paid Services)
+Members who wish to cancel a paid service must submit a cancellation request through customer support. The Company may immediately suspend and terminate paid services if the Member violates these Terms or uses the service improperly.
+
+Chapter 10. No Assignment
+Article 23 (No Assignment)
+Members may not assign, transfer, donate, or pledge their right to use the Service or their contractual status to a third party.
+
+Chapter 11. Governing Court
+Article 24 (Dispute Resolution)
+If a dispute arises in connection with the use of the Service, the Company and the Member shall make good-faith efforts to resolve it.
+
+Article 25 (Jurisdiction)
+Any dispute arising under these Terms shall be subject to the exclusive jurisdiction of the Seoul Central District Court.
+
+Supplementary Provision
+These Terms take effect on October 1, 2024.
""";
return _resolveAgreementText(
'msg.userfront.signup.tos_full',
fallback: fallback,
+ englishFallback: englishFallback,
placeholders: {'서비스 이용약관 전문...', 'Tos Full'},
);
}
@@ -1035,10 +1138,83 @@ class _SignupScreenState extends State {
회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.
제8조 (기타)
본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.
+""";
+ const englishFallback = """
+Consent to Collection and Use of Personal Information
+
+Baron Service Privacy Policy
+
+Article 1 (Purpose)
+Baron Consultant Co., Ltd. (the "Company") establishes this Privacy Policy to protect the personal information of customers and users of Baron Service (the "Service") and to fulfill its duties under the Personal Information Protection Act and other applicable laws.
+
+Article 2 (Purposes of Processing Personal Information)
+The Company processes personal information for the following purposes:
+- identity verification for registration and account management
+- communication by phone or email
+- provision of notices and operation of the Service
+- delivery of product materials
+- consultation and demo requests
+- event participation and seminar guidance
+- delivery of security guidance materials
+- technical support
+- service improvement feedback
+- marketing communications for users who have separately agreed
+
+Article 3 (Retention Period)
+The Company retains and uses personal information within the period required by law or agreed to by the data subject.
+- member information: from registration until 1 year after account deletion
+- promotional, consultation, and contract-related information: 2 years
+
+Article 4 (Provision to Third Parties)
+The Company processes personal information only within the scope described in this Policy and provides it to third parties only where consent has been obtained or where required by law.
+
+Article 5 (Entrustment of Processing)
+The Company does not currently entrust personal information processing to external processors for the core scope described here. If outsourcing becomes necessary, the Company will provide notice and obtain consent where required.
+
+Article 6 (Rights of Data Subjects)
+Data subjects may request access, correction, deletion, suspension of processing, and other rights permitted by law. Requests may be submitted in writing, by email, or by facsimile. The Company may verify the identity or authority of the requester.
+
+Article 7 (Items of Personal Information Processed)
+The Company may process the following items:
+- required: name, mobile phone number, email address
+- optional: company telephone number, inquiry details
+- collection channels: website, phone, email
+
+Article 8 (Destruction of Personal Information)
+When personal information is no longer needed due to expiration of the retention period or achievement of the processing purpose, the Company destroys it without delay. Electronic records are deleted using technically appropriate methods, and paper documents are shredded or incinerated.
+
+Article 9 (Security Measures)
+The Company implements administrative, technical, and physical safeguards, including internal management plans, employee training, access control, encryption where appropriate, security software, and restricted access to facilities.
+
+Article 10 (Automatic Collection Devices)
+The Company does not use cookies for this Service in the scope described here.
+
+Article 11 (Chief Privacy Officer)
+The Company designates a privacy officer responsible for overall personal information protection and complaint handling.
+
+Article 12 (Requests for Access)
+Data subjects may submit requests for access to personal information to the department designated by the Company, and the Company will make reasonable efforts to respond promptly.
+
+Article 13 (Remedies for Rights Infringement)
+Data subjects may seek dispute resolution or consultation from competent authorities and institutions handling personal information disputes and complaints.
+
+Article 14 (Changes to This Privacy Policy)
+If this Policy is added to, deleted from, or otherwise modified due to changes in law, policy, or security technology, the Company will provide advance notice before the effective date.
+
+Supplementary Provisions
+1. Effective Date
+This Privacy Policy takes effect on October 1, 2024.
+2. Notice of Amendments
+The Company will notify users of amendments through service notices, the website, or email as appropriate.
+3. Severability
+If any part of this Policy is held invalid or unenforceable, the remaining provisions will remain effective.
+4. Miscellaneous
+Matters not expressly provided in this Policy are governed by the Company's internal policies and applicable laws.
""";
return _resolveAgreementText(
'msg.userfront.signup.privacy_full',
fallback: fallback,
+ englishFallback: englishFallback,
placeholders: {'개인정보 수집 및 이용 동의 전문...', 'Privacy Full'},
);
}
diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart
index 5d1b4358..816137ed 100644
--- a/userfront/lib/i18n_data.dart
+++ b/userfront/lib/i18n_data.dart
@@ -34,9 +34,27 @@ const Map koStrings = {
"err.userfront.auth_proxy.consent_reject": "동의 거부에 실패했습니다.",
"err.userfront.auth_proxy.linked_app_revoke": "연동 해지에 실패했습니다.",
"err.userfront.auth_proxy.login_failed": "로그인에 실패했습니다.",
+ "err.userfront.auth_proxy.login_init": "로그인 초기화에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.login_poll": "로그인 상태 확인에 실패했습니다: {{error}}",
"err.userfront.auth_proxy.oidc_accept": "OIDC 로그인 승인에 실패했습니다.",
"err.userfront.auth_proxy.password_reset_complete": "비밀번호 재설정에 실패했습니다.",
+ "err.userfront.auth_proxy.password_policy_fetch": "비밀번호 정책을 불러오지 못했습니다.",
"err.userfront.auth_proxy.password_reset_init": "비밀번호 재설정을 시작하지 못했습니다.",
+ "err.userfront.auth_proxy.profile_load": "프로필을 불러오지 못했습니다: {{error}}",
+ "err.userfront.auth_proxy.tenant_info_fetch": "테넌트 정보를 불러오지 못했습니다.",
+ "err.userfront.auth_proxy.verify_failed": "검증에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.sms_send": "SMS 전송에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.code_verify": "인증 코드 확인에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.qr_init": "QR 로그인을 시작하지 못했습니다: {{error}}",
+ "err.userfront.auth_proxy.qr_poll": "QR 상태 확인에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.qr_approve": "QR 승인에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.user_create": "사용자 생성에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.user_list": "사용자 목록 조회에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.user_delete": "사용자 삭제에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.user_status_update": "상태 업데이트에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.user_update": "사용자 수정에 실패했습니다: {{error}}",
+ "err.userfront.auth_proxy.linked_apps_load": "연동된 앱 목록을 불러오지 못했습니다.",
+ "err.userfront.auth_proxy.phone_code_send": "인증 코드 전송에 실패했습니다: {{error}}",
"err.userfront.profile.load_failed": "프로필을 불러오지 못했습니다: {{error}}",
"err.userfront.profile.password_change_failed": "비밀번호 변경에 실패했습니다: {{error}}",
"err.userfront.profile.send_code_failed": "인증번호 전송 실패: {{error}}",
@@ -589,6 +607,7 @@ const Map koStrings = {
"재설정 링크가 만료되었습니다. 다시 요청해 주세요.",
"msg.userfront.error.whitelist.recovery_invalid": "재설정 링크가 유효하지 않습니다.",
"msg.userfront.error.whitelist.settings_disabled": "현재 계정 설정 화면은 준비 중입니다.",
+ "msg.userfront.error.whitelist.tenant_not_allowed": "허용되지 않은 테넌트입니다.",
"msg.userfront.error.whitelist.verification_required":
"추가 인증이 필요합니다. 안내에 따라 진행해 주세요.",
"msg.userfront.forgot.description":
@@ -2010,11 +2029,43 @@ const Map enStrings = {
"err.userfront.auth_proxy.linked_app_revoke":
"Failed to revoke the linked application.",
"err.userfront.auth_proxy.login_failed": "Login failed.",
+ "err.userfront.auth_proxy.login_init":
+ "Failed to initialize login: {{error}}",
+ "err.userfront.auth_proxy.login_poll":
+ "Failed to check login status: {{error}}",
"err.userfront.auth_proxy.oidc_accept": "OIDC Accept",
"err.userfront.auth_proxy.password_reset_complete":
"Failed to complete the password reset.",
+ "err.userfront.auth_proxy.password_policy_fetch":
+ "Failed to load the password policy.",
"err.userfront.auth_proxy.password_reset_init":
"Failed to start the password reset.",
+ "err.userfront.auth_proxy.profile_load":
+ "Failed to load the profile: {{error}}",
+ "err.userfront.auth_proxy.tenant_info_fetch":
+ "Failed to load tenant information.",
+ "err.userfront.auth_proxy.verify_failed": "Verification failed: {{error}}",
+ "err.userfront.auth_proxy.sms_send": "Failed to send SMS: {{error}}",
+ "err.userfront.auth_proxy.code_verify":
+ "Failed to verify the code: {{error}}",
+ "err.userfront.auth_proxy.qr_init": "Failed to start QR login: {{error}}",
+ "err.userfront.auth_proxy.qr_poll": "Failed to check QR status: {{error}}",
+ "err.userfront.auth_proxy.qr_approve":
+ "Failed to approve QR login: {{error}}",
+ "err.userfront.auth_proxy.user_create":
+ "Failed to create the user: {{error}}",
+ "err.userfront.auth_proxy.user_list":
+ "Failed to load the user list: {{error}}",
+ "err.userfront.auth_proxy.user_delete":
+ "Failed to delete the user: {{error}}",
+ "err.userfront.auth_proxy.user_status_update":
+ "Failed to update the user status: {{error}}",
+ "err.userfront.auth_proxy.user_update":
+ "Failed to update the user: {{error}}",
+ "err.userfront.auth_proxy.linked_apps_load":
+ "Failed to load linked applications.",
+ "err.userfront.auth_proxy.phone_code_send":
+ "Failed to send the verification code: {{error}}",
"err.userfront.profile.load_failed": "Failed to load the profile.",
"err.userfront.profile.password_change_failed": "Password Change Failed",
"err.userfront.profile.send_code_failed":
@@ -2669,6 +2720,8 @@ const Map enStrings = {
"The recovery link is invalid.",
"msg.userfront.error.whitelist.settings_disabled":
"Account settings are currently unavailable.",
+ "msg.userfront.error.whitelist.tenant_not_allowed":
+ "This tenant is not allowed.",
"msg.userfront.error.whitelist.verification_required":
"Additional verification is required. Please follow the instructions.",
"msg.userfront.forgot.description":
diff --git a/userfront/test/error_screen_test.dart b/userfront/test/error_screen_test.dart
index 6c38c57d..da3454c8 100644
--- a/userfront/test/error_screen_test.dart
+++ b/userfront/test/error_screen_test.dart
@@ -78,7 +78,7 @@ void main() {
);
final detail = tr(
'msg.userfront.error.whitelist.settings_disabled',
- fallback: internalErrorWhitelistMessages['settings_disabled']!,
+ fallback: tr(internalErrorWhitelistMessageKeys['settings_disabled']!),
);
final type = tr(
'msg.userfront.error.type',
@@ -160,7 +160,7 @@ void main() {
final detail = tr(
'msg.userfront.error.whitelist.not_found',
- fallback: internalErrorWhitelistMessages['not_found']!,
+ fallback: tr(internalErrorWhitelistMessageKeys['not_found']!),
);
final type = tr(
'msg.userfront.error.type',
@@ -185,7 +185,7 @@ void main() {
final detail = tr(
'msg.userfront.error.whitelist.rate_limited',
- fallback: internalErrorWhitelistMessages['rate_limited']!,
+ fallback: tr(internalErrorWhitelistMessageKeys['rate_limited']!),
);
final type = tr(
'msg.userfront.error.type',
@@ -214,28 +214,12 @@ void main() {
},
);
- final title = tr(
- 'msg.userfront.error.tenant.page_title',
- fallback: '애플리케이션 접근이 제한되었습니다',
- );
- final detail = tr(
- 'msg.userfront.error.tenant.detail',
- fallback: '현재 로그인된 계정은 이 애플리케이션에 접근할 수 없습니다.',
- );
- final account = tr('msg.userfront.error.tenant.account', fallback: '계정');
- final primaryTenant = tr(
- 'msg.userfront.error.tenant.primary_tenant',
- fallback: '대표 소속 테넌트',
- );
- final affiliatedTenants = tr(
- 'msg.userfront.error.tenant.affiliated_tenants',
- fallback: '전체 소속 테넌트',
- );
- final switchAccount = tr(
- 'ui.userfront.error.switch_account',
- fallback: '다른 계정으로 로그인',
- );
-
+ const title = 'Application access is restricted';
+ const detail =
+ 'The current signed-in account cannot access this application.';
+ const account = 'Account';
+ const primaryTenant = 'Primary affiliated tenant';
+ const affiliatedTenants = 'All affiliated tenants';
expect(find.text(title), findsOneWidget);
expect(find.text(detail), findsOneWidget);
expect(find.text(account), findsOneWidget);
@@ -243,7 +227,8 @@ void main() {
expect(find.text(primaryTenant), findsOneWidget);
expect(find.text(affiliatedTenants), findsOneWidget);
expect(find.text('Baron HQ'), findsNWidgets(2));
- expect(find.text(switchAccount), findsOneWidget);
+ expect(find.byType(ElevatedButton), findsOneWidget);
+ expect(find.byType(OutlinedButton), findsOneWidget);
});
testWidgets('tenant_not_allowed는 details를 우선 사용해 계정과 테넌트 정보를 노출한다', (