task(settings): Move legal doc query to gql

Because:
- We want to move our legal doc query to the server side so it can be made with a single call.

This Commit:
- Changes to clone script:
  - Use the provided state.json provided by l10n repo
  - Moves download to external folder
- Changes to settings:
  - Use gql to fetch legal doc
- Changes to gql
  - Adds ability to resolve a legal document.
This commit is contained in:
dschom 2023-03-22 16:10:29 -07:00
Родитель 2358852361
Коммит b843d2bb94
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: F26AEE99174EE68B
25 изменённых файлов: 509 добавлений и 109 удалений

Просмотреть файл

@ -5,8 +5,9 @@
# and file into `public/legal-docs` if present.
#
# Additionally, this script outputs `legal-docs/[name]_locales.json` with an array of locales
# (directories) that the file was found in to make it easy to know what's available instead
# of traversing directories when determining which locale to show the user.
# that indicating the locales that are supported. These json files originate from the
# .github/stats.json file in the legal-docs repo.
#
# ensure the script errors out when a command fails
set -e
@ -67,14 +68,18 @@ get_realpath_missing() {
}
# END: workaround for missing GNU realpath in BSD/MacOS-X
DOWNLOAD_PATH="${FXA_LEGAL_DOCS_DOWNLOAD_PATH:-legal-docs}"
SCRIPT_DIR="$(dirname "$0")"
DOWNLOAD_PATH="${FXA_LEGAL_DOCS_DOWNLOAD_PATH:-$(get_realpath_missing "$SCRIPT_DIR/../external/legal-docs")}"
MODULE_PATH="$(get_realpath_missing "$SCRIPT_DIR/../packages/$MODULE")"
LOCAL_PATH="$MODULE_PATH/public/legal-docs"
[ -d "$MODULE_PATH" ] || error "The module/package does not exist at: $MODULE_PATH"
# Clone and pull
cd "$MODULE_PATH"
echo "MODULE_PATH: $MODULE_PATH"
echo "LOCAL_PATH: $LOCAL_PATH"
echo "DOWNLOAD_PATH: $DOWNLOAD_PATH"
mkdir -p "$LOCAL_PATH"
if [ -n "${FXA_LEGAL_DOCS_COPY_FROM}" ]; then
echo "Using LEGAL_DOCS files from: ${FXA_LEGAL_DOCS_COPY_FROM}"
@ -130,18 +135,22 @@ copy_md() {
log "Copying $md_name and parent locale directories into $MODULE_PATH/public/legal-docs/"
cd "$DOWNLOAD_PATH"
results=()
for src in **"/${md_name}.md"; do
[ -f "$src" ] || continue
legal_docs_dir="$MODULE_PATH/public/legal-docs/$(dirname "$src" | sed -E 's/^.*\/([a-zA-Z_]+)\/.+/\1/; s/_/-/g')"
legal_docs_dir="$LOCAL_PATH/$(dirname "$src" | sed -E 's/^.*\/([a-zA-Z_]+)\/.+/\1/; s/_/-/g')"
[ -d "$legal_docs_dir" ] || mkdir -p "$legal_docs_dir"
cp "$src" "$legal_docs_dir/$md_name.md"
results+=("$(echo "$legal_docs_dir" | sed -E 's/.+\/([a-zA-Z_]+)/\1/; s/-/_/g')")
done
log "Creating .json file containing array of available locales in $MODULE_PATH/public/legal-docs/${md_name}_locales.json"
cat "$LOCAL_PATH/stats.json" | jq ".\"${md_name}.md\".locales" > "$MODULE_PATH/public/legal-docs/${md_name}_locales.json"
}
echo "[$(echo "${results[@]}" | tr ' ' ',' | sed -E 's/([^,]+)/"\1"/g')]" > "$MODULE_PATH/public/legal-docs/${md_name}_locales.json"
copy_json() {
cd "$LOCAL_PATH"
curl https://raw.githubusercontent.com/mozilla/legal-docs/l10n_reference/.github/stats.json > stats.json
}
SETTINGS="fxa-settings"
@ -149,6 +158,9 @@ SETTINGS="fxa-settings"
# Copy .md files into specified packages
case "$MODULE" in
"$SETTINGS")
mkdir -p "$MODULE/public/legal-docs"
cd "$MODULE/public/legal-docs"
copy_json
copy_md "firefox_privacy_notice" # legal/privacy page
copy_md "firefox_cloud_services_tos" # legal/terms page
;;

Просмотреть файл

@ -22,7 +22,8 @@
"generate-lockfile": "docker build . -f _dev/docker/ci-lockfile-generator/Dockerfile -t generate-lockfile && docker run generate-lockfile > yarn.lock",
"l10n:clone": "_scripts/l10n/clone.sh",
"l10n:prime": "_scripts/l10n/prime.sh",
"l10n:bundle": "_scripts/l10n/bundle.sh"
"l10n:bundle": "_scripts/l10n/bundle.sh",
"legal:clone": "_scripts/clone-legal-docs.sh"
},
"homepage": "https://github.com/mozilla/fxa",
"bugs": {

Просмотреть файл

@ -108,6 +108,11 @@ function modifyProxyRes(proxyRes, req, res) {
});
res.send(new Buffer.from(html));
} else {
// remove transfer-encoding header, a Content-Length header will be added
// automatically, and these two headers are incompatible. If this is not
// set any fetch request will fail with:
// - Parse Error: Content-Length can't be present with Transfer-Encoding
res.set('Transfer-Encoding', '');
res.send(body);
}

Просмотреть файл

@ -5,9 +5,10 @@ import { Module } from '@nestjs/common';
import { AuthClientFactory, AuthClientService } from './auth-client.service';
import { ProfileClientService } from './profile-client.service';
import { LegalService } from './legal.service';
@Module({
providers: [AuthClientFactory, ProfileClientService],
exports: [AuthClientService, ProfileClientService],
providers: [AuthClientFactory, ProfileClientService, LegalService],
exports: [AuthClientService, ProfileClientService, LegalService],
})
export class BackendModule {}

Просмотреть файл

@ -0,0 +1,139 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Provider, Res } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { LegalService } from './legal.service';
async function mockFetch(url: RequestInfo | URL, _init?: RequestInit) {
const resp = new Response();
if (typeof url === 'string') {
if (
url.includes(
'/settings/legal-docs/firefox_cloud_services_tos_locales.json'
)
) {
return {
...resp,
ok: true,
json: async () => ['en', 'es'],
};
}
if (url.includes('en/firefox_cloud_services_tos')) {
return {
...resp,
ok: true,
text: async () => '# Firefox Cloud Services: Terms of Service',
};
}
if (url.includes('es/firefox_cloud_services_tos')) {
return {
...resp,
ok: true,
text: async () => '# Firefox Cloud Services: Condiciones del servicio',
};
}
}
return {
...resp,
ok: true,
text: async () => '',
};
}
// jest.mock('node-fetch', () => fetch);
describe.only('#unit - LegalService', () => {
let service: LegalService;
beforeEach(async () => {
jest.resetModules();
global.fetch = jest.fn(mockFetch);
const MockConfig: Provider = {
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('http://localhost:3030'),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [MockConfig, LegalService],
}).compile();
service = module.get<LegalService>(LegalService);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('returns english doc', async () => {
const doc = await service.getDoc('en', 'firefox_cloud_services_tos');
expect(doc).toBeDefined();
expect(doc.markdown).toBeDefined();
expect(
/# Firefox Cloud Services: Terms of Service/.test(doc.markdown)
).toBeTruthy();
});
it('returns spanish doc', async () => {
const doc = await service.getDoc('es', 'firefox_cloud_services_tos');
expect(doc).toBeDefined();
expect(doc.markdown).toBeDefined();
expect(
/# Firefox Cloud Services: Condiciones del servicio/.test(doc.markdown)
).toBeTruthy();
});
it('returns spanish doc for unsupported dialect', async () => {
const doc = await service.getDoc('es-ar', 'firefox_cloud_services_tos');
expect(doc).toBeDefined();
expect(doc.markdown).toBeDefined();
expect(
/# Firefox Cloud Services: Condiciones del servicio/.test(doc.markdown)
).toBeTruthy();
});
it('returns empty string for bogus doc', async () => {
const doc = await service.getDoc('en', 'xyz');
expect(doc).toBeDefined();
expect(doc.markdown).toBeDefined();
expect(doc.markdown).toEqual('');
});
describe('invalid files', () => {
async function test(fileName: string) {
await expect(async () => {
await service.getDoc('en', fileName);
}).rejects.toThrow('Invalid file name');
}
it('rejects empty file name', async () => {
await test('');
});
it('rejects relative path like file name', async () => {
await test('../foo.txt');
});
it('rejects absolute path like file name', async () => {
await test('/foo/bar.txt');
});
it('rejects long file name', async () => {
await test('a'.repeat(1000));
});
});
});

Просмотреть файл

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ConfigService } from '@nestjs/config';
import { AppConfig } from '../config';
import { determineLocale } from 'fxa-shared/l10n/determineLocale';
import { Injectable } from '@nestjs/common';
const LEGAL_DOCS_PATH = '/settings/legal-docs';
@Injectable()
export class LegalService {
private settingsUrl: string;
constructor(configService: ConfigService<AppConfig>) {
this.settingsUrl = configService.get(
'settingsUrl'
) as AppConfig['settingsUrl'];
}
public async getDoc(locale: string, file: string) {
if (/^[a-zA-Z-_]{1,500}$/.test(file) === false) {
throw new Error(`Invalid file name`);
}
// Determine the best local to use, given the available ones
const availableLocales = await this.getAvailableLocales(file);
if (!availableLocales) {
return { markdown: '' };
}
locale = determineLocale(locale, availableLocales)?.replace('_', '-');
// Try to get the document from the external legal-docs repo. Note that
// since this is an external repo which might be in a transient state some
// fallback logic has been added. For example, de-DE would turn into de if
// the de-DE document did not exist.
let markdown = await this.tryGetDoc(locale, file);
if (!markdown && locale !== locale.replace('-.*', '')) {
markdown = await this.tryGetDoc(locale.replace('-.*', ''), file);
}
if (!markdown && locale !== 'en') {
markdown = await this.tryGetDoc('en', file);
}
return { markdown };
}
private async tryGetDoc(locale: string, file: string) {
const url = `${this.settingsUrl}${LEGAL_DOCS_PATH}/${locale}/${file}.md`;
const resp = await fetch(url);
const resText = await resp.text();
// The response should be markdown. Html is returned if the file is not found.
if (resText.includes('<!DOCTYPE html>')) {
return '';
}
return resText;
}
private async getAvailableLocales(file: string) {
const url = `${this.settingsUrl}${LEGAL_DOCS_PATH}/${file}_locales.json`;
const response = await fetch(`${url}`);
if (!response.ok) {
throw new Error(response.statusText);
}
try {
const availableLocales = await response.json();
return availableLocales as string[];
} catch {
return '';
}
}
}

Просмотреть файл

@ -58,6 +58,11 @@ const conf = convict({
default: 'http://localhost:7000',
env: 'CUSTOMS_SERVER_URL',
},
settingsUrl: {
doc: 'Settings server url',
default: 'http://localhost:3030',
env: 'SETTINGS_SERVER_URL',
},
database: {
mysql: {
auth: makeMySQLConfig('AUTH', 'fxa'),

Просмотреть файл

@ -26,3 +26,4 @@ export { VerifyTotpInput } from './verify-totp';
export { AccountStatusInput } from './account-status';
export { RecoveryKeyBundleInput } from './recovery-key';
export { PasswordChangeInput } from './password-change';
export { LegalInput } from './legal';

Просмотреть файл

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Field, InputType } from '@nestjs/graphql';
@InputType()
export class LegalInput {
@Field({
description: 'The requested l10n locale.',
nullable: true,
})
public locale!: string;
@Field({ description: 'The requested legal file.' })
public file!: string;
}

Просмотреть файл

@ -18,3 +18,4 @@ export { VerifyTotpPayload } from './verify-totp';
export { AccountStatusPayload } from './account-status';
export { RecoveryKeyBundlePayload } from './recovery-key';
export { PasswordChangePayload } from './password-change';
export { LegalDoc } from './legal';

Просмотреть файл

@ -0,0 +1,11 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class LegalDoc {
@Field({ description: 'Document in markdown format' })
public markdown!: string;
}

Просмотреть файл

@ -24,6 +24,7 @@ import { BackendModule } from '../backend/backend.module';
import Config, { AppConfig } from '../config';
import { AccountResolver } from './account.resolver';
import { SessionResolver } from './session.resolver';
import { LegalResolver } from './legal.resolver';
import { Request, Response } from 'express';
const config = Config.getProperties();
@ -65,15 +66,19 @@ export const GraphQLConfigFactory = async (
@Module({
imports: [BackendModule, CustomsModule],
providers: [AccountResolver, CustomsService, SessionResolver],
providers: [AccountResolver, CustomsService, SessionResolver, LegalResolver],
})
export class GqlModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req: Request, res: Response, next: Function) => {
if (config.env !== 'development' && !req.is('application/json') && !req.is('multipart/form-data')) {
if (
config.env !== 'development' &&
!req.is('application/json') &&
!req.is('multipart/form-data')
) {
return next(
new HttpException('Request content type is not supported.', 415),
new HttpException('Request content type is not supported.', 415)
);
}
next();

Просмотреть файл

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Test, TestingModule } from '@nestjs/testing';
import { LegalService } from '../backend/legal.service';
import { LegalResolver } from './legal.resolver';
describe('#unit - LegalResolver', () => {
let resolver: LegalResolver;
let mockLegalService: Partial<LegalService>;
beforeEach(async () => {
mockLegalService = {
getDoc: jest.fn().mockReturnValue('[]'),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
LegalResolver,
{ provide: LegalService, useValue: mockLegalService },
],
}).compile();
resolver = module.get<LegalResolver>(LegalResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
it('calls legal service', async () => {
await resolver.getLegalDoc({ locale: 'en', file: 'foo' });
expect(mockLegalService.getDoc).toBeCalledWith('en', 'foo');
});
});

Просмотреть файл

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Args, Query, Resolver } from '@nestjs/graphql';
import { LegalInput } from './dto/input';
import { LegalDoc } from './dto/payload';
import { LegalService } from '../backend/legal.service';
@Resolver()
export class LegalResolver {
constructor(private legalService: LegalService) {}
@Query((returns) => LegalDoc)
public async getLegalDoc(
@Args('input', { type: () => LegalInput })
input: LegalInput
) {
return this.legalService.getDoc(input.locale, input.file);
}
}

Просмотреть файл

@ -0,0 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ObjectType } from '@nestjs/graphql';
@ObjectType({ description: 'Session status' })
export class Legal {}

Просмотреть файл

@ -4,27 +4,27 @@
"homepage": "https://accounts.firefox.com/settings",
"private": true,
"scripts": {
"postinstall": "../../_scripts/clone-legal-docs.sh fxa-settings",
"build-css": "tailwindcss -i ./src/styles/tailwind.css -o ./src/styles/tailwind.out.css --postcss",
"build-storybook": "NODE_ENV=production STORYBOOK_BUILD=1 yarn build-css && NODE_OPTIONS=--openssl-legacy-provider build-storybook",
"build": " tsc --build ../fxa-react && NODE_ENV=production yarn build-css && yarn merge-ftl && SKIP_PREFLIGHT_CHECK=true INLINE_RUNTIME_CHUNK=false NODE_OPTIONS=--openssl-legacy-provider rescripts build",
"build-storybook": "NODE_ENV=production STORYBOOK_BUILD=1 yarn build-css && yarn legal-clone && NODE_OPTIONS=--openssl-legacy-provider build-storybook",
"build": "tsc --build ../fxa-react && NODE_ENV=production yarn build-css && yarn legal-clone && yarn merge-ftl && SKIP_PREFLIGHT_CHECK=true INLINE_RUNTIME_CHUNK=false NODE_OPTIONS=--openssl-legacy-provider rescripts build",
"compile": "tsc --noEmit",
"clean": "git clean -fXd",
"eject": "react-scripts eject",
"l10n-prime": "yarn l10n:prime fxa-settings",
"l10n-bundle": "yarn l10n:bundle fxa-settings branding,react,settings",
"legal-clone": "yarn legal:clone fxa-settings",
"lint:eslint": "eslint . .storybook",
"lint": "npm-run-all --parallel lint:eslint",
"restart": "npm run build-css && pm2 restart pm2.config.js",
"start": "yarn merge-ftl && npm run build-css && pm2 start pm2.config.js && ../../_scripts/check-url.sh localhost:3000/settings/static/js/bundle.js",
"stop": "pm2 stop pm2.config.js",
"delete": "pm2 delete pm2.config.js",
"storybook": "STORYBOOK_BUILD=1 npm run build-css && NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6008 --no-version-updates",
"test": "yarn merge-ftl:test && SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false",
"test:watch": "yarn merge-ftl:test && SKIP_PREFLIGHT_CHECK=true rescripts test",
"test:coverage": "yarn test --coverage --watchAll=false",
"storybook": "yarn legal-clone && STORYBOOK_BUILD=1 npm run build-css && NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6008 --no-version-updates",
"test": "yarn legal-clone && yarn merge-ftl:test && SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false",
"test:watch": "yarn legal-clone && yarn merge-ftl:test && SKIP_PREFLIGHT_CHECK=true rescripts test",
"test:coverage": "yarn legal-clone && yarn test --coverage --watchAll=false",
"test:unit": "echo No unit tests present for $npm_package_name",
"test:integration": "yarn merge-ftl:test && tsc --build ../fxa-react && JEST_JUNIT_OUTPUT_FILE=../../artifacts/tests/$npm_package_name/jest-integration.xml SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false --ci --runInBand --reporters=default --reporters=jest-junit",
"test:integration": "yarn legal-clone && yarn merge-ftl:test && tsc --build ../fxa-react && JEST_JUNIT_OUTPUT_FILE=../../artifacts/tests/$npm_package_name/jest-integration.xml SKIP_PREFLIGHT_CHECK=true rescripts test --watchAll=false --ci --runInBand --reporters=default --reporters=jest-junit",
"merge-ftl": "yarn l10n-prime && grunt merge-ftl && yarn l10n-bundle",
"merge-ftl:test": "yarn l10n-prime && grunt merge-ftl:test",
"watch-ftl": "grunt watch-ftl"

Просмотреть файл

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import AppLayout from '../AppLayout';
import { navigate } from '@reach/router';
import { FtlMsg } from 'fxa-react/lib/utils';
@ -13,6 +13,21 @@ import Banner, { BannerType } from '../Banner';
import LoadingSpinner from 'fxa-react/components/LoadingSpinner';
import { REACT_ENTRYPOINT } from '../../constants';
import { fetchLegalMd, LegalDocFile } from '../../lib/file-utils-legal';
import { AppContext } from '../../models';
export type FetchLegalDoc = (
locale: string,
legalDocFile: string
) => Promise<{ markdown?: string; error?: string }>;
export type LegalWithMarkdownProps = {
locale?: string;
viewName: 'legal-privacy' | 'legal-terms';
legalDocFile: LegalDocFile;
headingTextFtlId: string;
headingText: string;
fetchLegalDoc?: FetchLegalDoc;
};
const LegalWithMarkdown = ({
locale,
@ -20,23 +35,25 @@ const LegalWithMarkdown = ({
legalDocFile,
headingTextFtlId,
headingText,
}: {
locale?: string;
viewName: 'legal-privacy' | 'legal-terms';
legalDocFile: LegalDocFile;
headingTextFtlId: string;
headingText: string;
}) => {
fetchLegalDoc,
}: LegalWithMarkdownProps) => {
usePageViewEvent(viewName, REACT_ENTRYPOINT);
const [markdown, setMarkdown] = useState<string | undefined>();
const [error, setError] = useState<string | undefined>();
const { apolloClient } = useContext(AppContext);
useEffect(() => {
let isMounted = true;
(async () => {
const { markdown: fetchedMarkdown, error } = await fetchLegalMd(
navigator.languages,
locale,
async function fetchLegal(locale: string, legalDocFile: string) {
if (fetchLegalDoc != null) {
return fetchLegalDoc(locale, legalDocFile);
}
return fetchLegalMd(apolloClient, locale, legalDocFile);
}
const { markdown: fetchedMarkdown, error } = await fetchLegal(
locale || navigator.language || 'en',
legalDocFile
);
// ensure component is still mounted before trying to render (fixes state update warning)
@ -52,7 +69,7 @@ const LegalWithMarkdown = ({
return () => {
isMounted = false;
};
}, [locale, legalDocFile]);
}, [locale, legalDocFile, apolloClient, fetchLegalDoc]);
const buttonHandler = () => {
logViewEvent(`flow.${viewName}`, 'back', REACT_ENTRYPOINT);

Просмотреть файл

@ -2,74 +2,48 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { determineLocale } from 'fxa-shared/l10n/determineLocale';
import sentryMetrics from 'fxa-shared/lib/sentry';
import { GET_LEGAL_DOC } from '../models';
import { ApolloClient, gql } from '@apollo/client';
export enum LegalDocFile {
privacy = 'firefox_privacy_notice',
terms = 'firefox_cloud_services_tos',
}
const LEGAL_DOCS_PATH = '/settings/legal-docs';
// TODO: probably move this + clone script to gql-api to reduce network requests
const fetchLegalMdByLocale = async (locale: string, file: LegalDocFile) => {
// use the general app error for now, might change when moving this to gql-api
const error = 'Something went wrong. Please try again later.';
try {
const response = await fetch(`${LEGAL_DOCS_PATH}/${locale}/${file}.md`);
if (response.ok) {
const resText = await response.text();
if (resText.includes('<!DOCTYPE html>')) {
return { error };
}
return { markdown: resText };
} else {
throw Error(response.statusText);
}
} catch (e) {
sentryMetrics.captureException(e);
// TODO: If the first preferred language can't be loaded, try
// the next 2? and then fallback to English + clean this up
if (locale !== 'en') {
try {
const response = await fetch(`${LEGAL_DOCS_PATH}/en/${file}.md`);
if (response.ok) {
const resText = await response.text();
if (resText.includes('<!DOCTYPE html>')) {
return { error };
}
return { markdown: resText };
}
} catch (e) {
sentryMetrics.captureException(e);
return { error };
}
}
return { error };
}
};
export const fetchLegalMd = async (
acceptLanguages: readonly string[],
localeParam: string | undefined,
file: LegalDocFile
) => {
let availableLocales;
try {
const response = await fetch(`${LEGAL_DOCS_PATH}/${file}_locales.json`);
availableLocales = await response.json();
} catch (e) {
// report to Sentry and allow default locales to be loaded
sentryMetrics.captureException(e);
apolloClient: ApolloClient<object> | undefined,
locale: string,
file: string
): Promise<{
markdown?: string;
error?: string;
}> => {
const error = `Something went wrong. Try again later.`;
if (apolloClient == null) {
console.error('No apolloClient provided.');
return {
error,
};
}
const locale = determineLocale(
localeParam ? localeParam : acceptLanguages.join(', '),
availableLocales
);
return fetchLegalMdByLocale(locale, file);
try {
const result = await apolloClient.query({
query: GET_LEGAL_DOC,
variables: { input: { locale, file } },
});
if (result?.data?.getLegalDoc?.markdown) {
return {
markdown: result.data.getLegalDoc?.markdown,
};
}
// If the markdown we got back is empty / invalid error out.
throw new Error(error);
} catch (err) {
return {
error,
};
}
};

Просмотреть файл

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { gql } from '@apollo/client';
export interface LegalDoc {
markdown: string;
}
export const GET_LEGAL_DOC = gql`
query GetLegalDoc($input: LegalInput!) {
getLegalDoc(input: $input) {
markdown
}
}
`;

Просмотреть файл

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export * from './AppContext';
export * from './AlertBarInfo';
export * from './Account';
@ -5,3 +9,4 @@ export * from './Session';
export * from './hooks';
export * from './verification';
export * from './reliers';
export * from './Legal';

Просмотреть файл

@ -6,6 +6,7 @@ import React from 'react';
import LegalPrivacy from '.';
import { Meta } from '@storybook/react';
import { withLocalization } from '../../../../.storybook/decorators';
import { fetchLegalDoc } from '../mocks';
export default {
title: 'Pages/Legal/Privacy',
@ -13,4 +14,4 @@ export default {
decorators: [withLocalization],
} as Meta;
export const Basic = () => <LegalPrivacy />;
export const Basic = () => <LegalPrivacy {...{ fetchLegalDoc }} />;

Просмотреть файл

@ -4,20 +4,30 @@
import React from 'react';
import { RouteComponentProps } from '@reach/router';
import LegalWithMarkdown from '../../../components/LegalWithMarkdown';
import LegalWithMarkdown, {
FetchLegalDoc,
} from '../../../components/LegalWithMarkdown';
import { LegalDocFile } from '../../../lib/file-utils-legal';
export const viewName = 'legal-privacy';
export type LegalPrivacyProps = {
locale?: string;
fetchLegalDoc?: FetchLegalDoc;
};
const LegalPrivacy = ({
locale,
}: { locale?: string } & RouteComponentProps) => (
<LegalWithMarkdown
{...{ locale, viewName }}
legalDocFile={LegalDocFile.privacy}
headingTextFtlId="legal-privacy-heading"
headingText="Privacy Notice"
/>
);
fetchLegalDoc,
}: LegalPrivacyProps & RouteComponentProps) => {
return (
<LegalWithMarkdown
{...{ locale, fetchLegalDoc, viewName }}
headingTextFtlId="legal-privacy-heading"
headingText="Privacy Notice"
legalDocFile={LegalDocFile.privacy}
/>
);
};
export default LegalPrivacy;

Просмотреть файл

@ -6,6 +6,7 @@ import React from 'react';
import LegalTerms from '.';
import { Meta } from '@storybook/react';
import { withLocalization } from '../../../../.storybook/decorators';
import { fetchLegalDoc } from '../mocks';
export default {
title: 'Pages/Legal/Terms',
@ -13,4 +14,4 @@ export default {
decorators: [withLocalization],
} as Meta;
export const Basic = () => <LegalTerms />;
export const Basic = () => <LegalTerms {...{ fetchLegalDoc }} />;

Просмотреть файл

@ -4,14 +4,24 @@
import React from 'react';
import { RouteComponentProps } from '@reach/router';
import LegalWithMarkdown from '../../../components/LegalWithMarkdown';
import LegalWithMarkdown, {
FetchLegalDoc,
} from '../../../components/LegalWithMarkdown';
import { LegalDocFile } from '../../../lib/file-utils-legal';
export const viewName = 'legal-terms';
const LegalTerms = ({ locale }: { locale?: string } & RouteComponentProps) => (
export type LegalTermsProps = {
locale?: string;
fetchLegalDoc?: FetchLegalDoc;
};
const LegalTerms = ({
locale,
fetchLegalDoc,
}: LegalTermsProps & RouteComponentProps) => (
<LegalWithMarkdown
{...{ locale, viewName }}
{...{ locale, fetchLegalDoc, viewName }}
headingTextFtlId="legal-terms-heading"
headingText="Terms of Service"
legalDocFile={LegalDocFile.terms}

Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { FetchLegalDoc } from '../../components/LegalWithMarkdown';
export const fetchLegalDoc: FetchLegalDoc = async (locale, legalDocFile) => {
for (const fallback of [locale, locale.replace(/-.*/, ''), 'en']) {
let markdown = await fetchDoc(fallback, legalDocFile);
// We can assume that the first line of the markdown file is a heading.
if (markdown.trim().startsWith('#')) {
return { markdown };
}
}
return { error: 'Not found' };
};
async function fetchDoc(locale: string, legalDocFile: string) {
const path = `/legal-docs/${locale}/${legalDocFile}.md`;
const response = await fetch(path);
if (response.ok) {
return await response.text();
}
return '';
}