Skip to content

Commit 2ee07bc

Browse files
authored
Merge pull request #2369 from tf/opt-in-improvements
Opt in improvements
2 parents 892f7ca + 0c0b070 commit 2ee07bc

18 files changed

Lines changed: 406 additions & 79 deletions

File tree

entry_types/scrolled/app/helpers/pageflow_scrolled/editor/entry_json_seed_helper.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def scrolled_entry_editor_json_seed(json, scrolled_entry)
1111
entry_config = Pageflow.config_for(scrolled_entry)
1212

1313
scrolled_entry_editor_legacy_typography_variants_seed(json, entry_config)
14-
scrolled_entry_editor_consent_vendor_host_matchers_seed(json, entry_config)
14+
scrolled_entry_editor_consent_vendor_url_matchers_seed(json, entry_config)
1515

1616
scrolled_entry_json_seed(json,
1717
scrolled_entry,
@@ -32,10 +32,10 @@ def scrolled_entry_editor_legacy_typography_variants_seed(json, entry_config)
3232
)
3333
end
3434

35-
def scrolled_entry_editor_consent_vendor_host_matchers_seed(json, entry_config)
36-
json.consent_vendor_host_matchers(
35+
def scrolled_entry_editor_consent_vendor_url_matchers_seed(json, entry_config)
36+
json.consent_vendor_url_matchers(
3737
entry_config
38-
.consent_vendor_host_matchers
38+
.consent_vendor_url_matchers
3939
.transform_keys { |regexp| regexp.inspect[1..-2] }
4040
)
4141
end

entry_types/scrolled/lib/pageflow_scrolled/configuration.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,32 @@ class Configuration
8080
# @since 16.1
8181
attr_reader :content_element_consent_vendors
8282

83-
# Mapping from URL hosts to consent vendor names. Used for iframe
84-
# embed opt-in.
83+
# Mapping from URL host+path patterns to consent vendor names.
84+
# Used for iframe embed opt-in.
8585
#
8686
# @exmaple
8787
#
88-
# entry_type_config.consent_vendor_host_matchers = {
89-
# /\.some-vendor\.com$/ => 'someVendor'
88+
# entry_type_config.consent_vendor_url_matchers = {
89+
# /\.some-vendor\.com\// => 'someVendor',
90+
# /google\.com\/maps\/embed/ => 'googleMaps'
9091
# }
9192
#
9293
# @return [Hash<RegExp, String>]
94+
# @since edge
95+
attr_accessor :consent_vendor_url_matchers
96+
97+
# @deprecated Use {#consent_vendor_url_matchers=} instead.
98+
# Rewrites trailing `$` in patterns to `/` and merges
99+
# into {#consent_vendor_url_matchers}.
93100
# @since 16.1
94-
attr_accessor :consent_vendor_host_matchers
101+
def consent_vendor_host_matchers=(matchers)
102+
rewritten = matchers.transform_keys do |key|
103+
source = key.source.sub(/\$$/, '/')
104+
Regexp.new(source, key.options)
105+
end
106+
107+
@consent_vendor_url_matchers.merge!(rewritten)
108+
end
95109

96110
# Migrate typography variants to palette colors. Before palette
97111
# colors for text blocks and headings were introduced, it was
@@ -155,7 +169,7 @@ def initialize(*)
155169
@additional_frontend_seed_data = AdditionalSeedData.new
156170
@additional_theme_assets = AdditionalThemeAssets.new
157171
@content_element_consent_vendors = ContentElementConsentVendors.new
158-
@consent_vendor_host_matchers = {}
172+
@consent_vendor_url_matchers = {}
159173

160174
@legacy_typography_variants = {}
161175
end

entry_types/scrolled/lib/pageflow_scrolled/plugin.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,10 @@ def configure(config)
227227
return unless configuration['requireConsent']
228228

229229
uri = URI.parse(configuration['source'])
230-
host_matchers = Pageflow.config_for(entry).consent_vendor_host_matchers
230+
url_matchers = Pageflow.config_for(entry).consent_vendor_url_matchers
231231

232-
host_matchers.detect { |matcher, _|
233-
uri.host =~ matcher
232+
url_matchers.detect { |matcher, _|
233+
(uri.host + uri.path) =~ matcher
234234
}&.last
235235
rescue URI::InvalidURIError
236236
nil

entry_types/scrolled/package/spec/editor/models/ConsentVendors-spec.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import {ConsentVendors} from 'editor/models/ConsentVendors';
22

33
describe('ConsentVendors', () => {
44
describe('fromUrl', () => {
5-
it('detects vendor from seed data host matcher', () => {
5+
it('detects vendor from seed data url matcher', () => {
66
const consentVendors = new ConsentVendors({
7-
hostMatchers: {
8-
'\\.some-vendor.com$': 'someVendor'
7+
urlMatchers: {
8+
'\\.some-vendor\\.com/': 'someVendor'
99
}
1010
});
1111

@@ -14,5 +14,18 @@ describe('ConsentVendors', () => {
1414
expect(consentVendors.fromUrl('https://other.com/abc'))
1515
.toBeUndefined();
1616
});
17+
18+
it('detects vendor from path-based url matcher', () => {
19+
const consentVendors = new ConsentVendors({
20+
urlMatchers: {
21+
'google\\.com/maps/embed': 'googleMaps'
22+
}
23+
});
24+
25+
expect(consentVendors.fromUrl('https://www.google.com/maps/embed?pb=1234'))
26+
.toEqual('googleMaps');
27+
expect(consentVendors.fromUrl('https://www.google.com/search?q=test'))
28+
.toBeUndefined();
29+
});
1730
})
1831
})

entry_types/scrolled/package/spec/frontend/features/thirdPartyConsent-spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,42 @@ describe('Third party consent', () => {
338338

339339
expect(container.querySelector('[data-file-name=SvgMedia]')).not.toBeNull();
340340
});
341+
342+
it('renders privacy link pointing to privacy page with vendor param', async () => {
343+
const {getByTestId} = await renderEntry({
344+
seed: {
345+
themeOptions: {thirdPartyConsent: {cookieName: 'optIn'}},
346+
contentElements: [{typeName: 'test'}],
347+
legalInfo: {
348+
imprint: {label: '', url: ''},
349+
copyright: {label: '', url: ''},
350+
privacy: {label: 'Privacy', url: 'https://example.com/privacy'}
351+
}
352+
}
353+
});
354+
355+
const {getByText} = within(getByTestId('test-content-element'));
356+
const link = getByText('Privacy');
357+
358+
expect(link).toHaveAttribute(
359+
'href',
360+
'https://example.com/privacy?vendors=someService#consent'
361+
);
362+
expect(link).toHaveAttribute('target', '_blank');
363+
});
364+
365+
it('does not render privacy link when no privacy url is configured', async () => {
366+
const {getByTestId} = await renderEntry({
367+
seed: {
368+
themeOptions: {thirdPartyConsent: {cookieName: 'optIn'}},
369+
contentElements: [{typeName: 'test'}]
370+
}
371+
});
372+
373+
const {queryByText} = within(getByTestId('test-content-element'));
374+
375+
expect(queryByText('Privacy')).toBeNull();
376+
});
341377
});
342378

343379
describe('opt in with implicit provider name', () => {
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {usePrivacyLink} from 'frontend/usePrivacyLink';
2+
3+
import {renderHookInEntry} from 'support';
4+
5+
describe('usePrivacyLink', () => {
6+
it('returns url and label without modifications when called without args', () => {
7+
const {result} = renderHookInEntry(
8+
() => usePrivacyLink(), {
9+
seed: {
10+
legalInfo: {
11+
privacy: {url: 'https://example.com/privacy', label: 'Privacy'}
12+
}
13+
}
14+
}
15+
);
16+
17+
expect(result.current.url).toEqual('https://example.com/privacy');
18+
expect(result.current.label).toEqual('Privacy');
19+
});
20+
21+
it('returns empty string when privacy url is empty', () => {
22+
const {result} = renderHookInEntry(
23+
() => usePrivacyLink({vendors: 'spotify'}), {
24+
seed: {
25+
legalInfo: {
26+
privacy: {url: ''}
27+
}
28+
}
29+
}
30+
);
31+
32+
expect(result.current.url).toEqual('');
33+
});
34+
35+
it('appends vendors param and consent hash to absolute url', () => {
36+
const {result} = renderHookInEntry(
37+
() => usePrivacyLink({vendors: 'spotify'}), {
38+
seed: {
39+
legalInfo: {
40+
privacy: {url: 'https://example.com/privacy'}
41+
}
42+
}
43+
}
44+
);
45+
46+
expect(result.current.url)
47+
.toEqual('https://example.com/privacy?vendors=spotify#consent');
48+
});
49+
50+
it('appends vendors param to url with existing params', () => {
51+
const {result} = renderHookInEntry(
52+
() => usePrivacyLink({vendors: 'spotify'}), {
53+
seed: {
54+
legalInfo: {
55+
privacy: {url: 'https://example.com/privacy?lang=de'}
56+
}
57+
}
58+
}
59+
);
60+
61+
expect(result.current.url)
62+
.toEqual('https://example.com/privacy?lang=de&vendors=spotify#consent');
63+
});
64+
65+
it('preserves protocol-relative url format', () => {
66+
const {result} = renderHookInEntry(
67+
() => usePrivacyLink({vendors: 'spotify'}), {
68+
seed: {
69+
legalInfo: {
70+
privacy: {url: '//example.com/privacy?lang=de'}
71+
}
72+
}
73+
}
74+
);
75+
76+
expect(result.current.url)
77+
.toEqual('//example.com/privacy?lang=de&vendors=spotify#consent');
78+
});
79+
80+
it('handles relative path', () => {
81+
const {result} = renderHookInEntry(
82+
() => usePrivacyLink({vendors: 'spotify'}), {
83+
seed: {
84+
legalInfo: {
85+
privacy: {url: '/privacy?lang=de'}
86+
}
87+
}
88+
}
89+
);
90+
91+
expect(result.current.url)
92+
.toEqual('/privacy?lang=de&vendors=spotify#consent');
93+
});
94+
95+
it('supports comma-separated vendor names', () => {
96+
const {result} = renderHookInEntry(
97+
() => usePrivacyLink({vendors: 'spotify,youtube'}), {
98+
seed: {
99+
legalInfo: {
100+
privacy: {url: 'https://example.com/privacy'}
101+
}
102+
}
103+
}
104+
);
105+
106+
expect(result.current.url)
107+
.toEqual('https://example.com/privacy?vendors=spotify%2Cyoutube#consent');
108+
});
109+
110+
it('returns link props with href and target', () => {
111+
const {result} = renderHookInEntry(
112+
() => usePrivacyLink(), {
113+
seed: {
114+
legalInfo: {
115+
privacy: {url: 'https://example.com/privacy'}
116+
}
117+
}
118+
}
119+
);
120+
121+
expect(result.current.props).toEqual({
122+
href: 'https://example.com/privacy',
123+
target: '_blank',
124+
rel: 'noreferrer noopener'
125+
});
126+
});
127+
128+
it('returns props with onClick for javascript: privacy settings url', () => {
129+
const {result} = renderHookInEntry(
130+
// eslint-disable-next-line no-script-url
131+
() => usePrivacyLink(), {
132+
seed: {
133+
legalInfo: {
134+
privacy: {url: 'javascript:pageflowDisplayPrivacySettings()'}
135+
}
136+
}
137+
}
138+
);
139+
140+
expect(result.current.props.href).toEqual('#privacySettings');
141+
expect(result.current.props.onClick).toBeInstanceOf(Function);
142+
expect(result.current.props.target).toBeUndefined();
143+
});
144+
145+
it('returns link props with vendors param in href', () => {
146+
const {result} = renderHookInEntry(
147+
() => usePrivacyLink({vendors: 'spotify'}), {
148+
seed: {
149+
legalInfo: {
150+
privacy: {url: 'https://example.com/privacy'}
151+
}
152+
}
153+
}
154+
);
155+
156+
expect(result.current.props).toEqual({
157+
href: 'https://example.com/privacy?vendors=spotify#consent',
158+
target: '_blank',
159+
rel: 'noreferrer noopener'
160+
});
161+
});
162+
});

entry_types/scrolled/package/spec/widgets/defaultNavigation/LegalInfoLink-spec.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ describe('LegalInfoLink', () => {
1717
expect(getByRole('link')).toHaveAttribute('rel', 'noreferrer noopener');
1818
});
1919

20-
it('supports javascript url scheme', () => {
20+
it('allows overriding link props', () => {
2121
const {getByRole} = render(
22-
// eslint-disable-next-line no-script-url
23-
<LegalInfoLink label="Copyright" url="javascript:pageflowDisplayPrivacySettings()" />
24-
)
22+
<LegalInfoLink label="Privacy"
23+
url="https://example.com"
24+
props={{href: '#privacySettings', onClick: jest.fn()}} />
25+
);
2526

26-
expect(getByRole('link')).toHaveTextContent('Copyright');
27+
expect(getByRole('link')).toHaveTextContent('Privacy');
2728
expect(getByRole('link')).toHaveAttribute('href', '#privacySettings');
2829
expect(getByRole('link')).not.toHaveAttribute('target');
2930
expect(getByRole('link')).not.toHaveAttribute('rel');

entry_types/scrolled/package/src/editor/models/ConsentVendors.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
export function ConsentVendors({hostMatchers}) {
1+
export function ConsentVendors({urlMatchers}) {
22
return {
33
fromUrl(url) {
44
url = new URL(url);
55

6-
return Object.entries(hostMatchers).find(([matcher]) =>
7-
new RegExp(matcher).test(url.host)
6+
return Object.entries(urlMatchers).find(([matcher]) =>
7+
new RegExp(matcher).test(url.host + url.pathname)
88
)?.[1];
99
}
1010
}

entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const defaultAspectRatios = [{
4747

4848
export const ScrolledEntry = Entry.extend({
4949
setupFromEntryTypeSeed(seed) {
50-
this.consentVendors = new ConsentVendors({hostMatchers: seed.consentVendorHostMatchers});
50+
this.consentVendors = new ConsentVendors({urlMatchers: seed.consentVendorUrlMatchers});
5151

5252
this.contentElements = new ContentElementsCollection(seed.collections.contentElements);
5353
this.sections = new SectionsCollection(seed.collections.sections,

entry_types/scrolled/package/src/frontend/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export {useFloatingPortalRoot, FloatingPortalRootProvider} from './FloatingPorta
124124
export {SectionIntersectionObserver} from './SectionIntersectionObserver';
125125
export {ScrollButton} from './ScrollButton';
126126

127+
export {usePrivacyLink} from './usePrivacyLink';
127128
export {textColorForBackgroundColor} from './textColorForBackgroundColor';
128129
export {getTransitionNames, getAvailableTransitionNames} from './transitions';
129130
export {getAppearanceSectionScopeName} from './appearance';

0 commit comments

Comments
 (0)