зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
2358852361
Коммит
b843d2bb94
|
@ -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 '';
|
||||
}
|
Загрузка…
Ссылка в новой задаче