@pushwoosh/dumb-components 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PushStoriesPreview/PushStoriesPreview.d.ts +5 -0
- package/PushStoriesPreview/PushStoriesPreview.js +71 -0
- package/PushStoriesPreview/__tests__/PushStoriesPreview.test.d.ts +1 -0
- package/PushStoriesPreview/__tests__/PushStoriesPreview.test.js +234 -0
- package/PushStoriesPreview/components/PushStoriesPage/PushStoriesPage.d.ts +4 -0
- package/PushStoriesPreview/components/PushStoriesPage/PushStoriesPage.js +38 -0
- package/PushStoriesPreview/components/PushStoriesPage/index.d.ts +1 -0
- package/PushStoriesPreview/components/PushStoriesPage/index.js +1 -0
- package/PushStoriesPreview/components/PushStoriesPage/styles.d.ts +7 -0
- package/PushStoriesPreview/components/PushStoriesPage/styles.js +31 -0
- package/PushStoriesPreview/components/PushStoriesPage/types.d.ts +6 -0
- package/PushStoriesPreview/components/PushStoriesPage/types.js +1 -0
- package/PushStoriesPreview/components/PushStoriesProgress/PushStoriesProgress.d.ts +4 -0
- package/PushStoriesPreview/components/PushStoriesProgress/PushStoriesProgress.js +24 -0
- package/PushStoriesPreview/components/PushStoriesProgress/index.d.ts +1 -0
- package/PushStoriesPreview/components/PushStoriesProgress/index.js +1 -0
- package/PushStoriesPreview/components/PushStoriesProgress/styles.d.ts +5 -0
- package/PushStoriesPreview/components/PushStoriesProgress/styles.js +17 -0
- package/PushStoriesPreview/components/PushStoriesProgress/types.d.ts +15 -0
- package/PushStoriesPreview/components/PushStoriesProgress/types.js +1 -0
- package/PushStoriesPreview/constants.d.ts +8 -0
- package/PushStoriesPreview/constants.js +8 -0
- package/PushStoriesPreview/helpers.d.ts +7 -0
- package/PushStoriesPreview/helpers.js +19 -0
- package/PushStoriesPreview/index.d.ts +4 -0
- package/PushStoriesPreview/index.js +3 -0
- package/PushStoriesPreview/polyglot/index.d.ts +2 -0
- package/PushStoriesPreview/polyglot/index.js +34 -0
- package/PushStoriesPreview/polyglot/messages_ar.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_ar.js +4 -0
- package/PushStoriesPreview/polyglot/messages_bn.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_bn.js +4 -0
- package/PushStoriesPreview/polyglot/messages_de.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_de.js +4 -0
- package/PushStoriesPreview/polyglot/messages_en.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_en.js +4 -0
- package/PushStoriesPreview/polyglot/messages_es.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_es.js +4 -0
- package/PushStoriesPreview/polyglot/messages_fr.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_fr.js +4 -0
- package/PushStoriesPreview/polyglot/messages_he.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_he.js +4 -0
- package/PushStoriesPreview/polyglot/messages_hi.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_hi.js +4 -0
- package/PushStoriesPreview/polyglot/messages_ja.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_ja.js +4 -0
- package/PushStoriesPreview/polyglot/messages_ko.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_ko.js +4 -0
- package/PushStoriesPreview/polyglot/messages_pt.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_pt.js +4 -0
- package/PushStoriesPreview/polyglot/messages_ru.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_ru.js +4 -0
- package/PushStoriesPreview/polyglot/messages_th.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_th.js +4 -0
- package/PushStoriesPreview/polyglot/messages_zh.d.ts +3 -0
- package/PushStoriesPreview/polyglot/messages_zh.js +4 -0
- package/PushStoriesPreview/polyglot/types.d.ts +5 -0
- package/PushStoriesPreview/polyglot/types.js +1 -0
- package/PushStoriesPreview/styles.d.ts +6 -0
- package/PushStoriesPreview/styles.js +23 -0
- package/PushStoriesPreview/types.d.ts +32 -0
- package/PushStoriesPreview/types.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { PushStoriesPreviewProps } from './types';
|
|
3
|
+
/** Expanded iOS push story as `PushwooshStoriesViewController` draws it: 2:3 page, progress, scrim, button.
|
|
4
|
+
* Fills its container's width, so it fits a form column as well as the phone of a push preview. */
|
|
5
|
+
export declare function PushStoriesPreview(props: PushStoriesPreviewProps): ReactElement;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ImageIcon } from '@pushwoosh/kit-icons';
|
|
4
|
+
import { PushStoriesPage } from './components/PushStoriesPage';
|
|
5
|
+
import { PushStoriesProgress } from './components/PushStoriesProgress';
|
|
6
|
+
import { clampStoryIndex, normalizeStoryDuration } from './helpers';
|
|
7
|
+
import { Lang, useLang } from './polyglot';
|
|
8
|
+
import { EmptyState, StoriesCanvas, StoriesFrame, TapZone } from './styles';
|
|
9
|
+
/** Expanded iOS push story as `PushwooshStoriesViewController` draws it: 2:3 page, progress, scrim, button.
|
|
10
|
+
* Fills its container's width, so it fits a form column as well as the phone of a push preview. */
|
|
11
|
+
export function PushStoriesPreview(props) {
|
|
12
|
+
const {
|
|
13
|
+
pages,
|
|
14
|
+
activeIndex,
|
|
15
|
+
defaultActiveIndex = 0,
|
|
16
|
+
onActiveIndexChange,
|
|
17
|
+
autoplay = false,
|
|
18
|
+
interactive = true,
|
|
19
|
+
width
|
|
20
|
+
} = props;
|
|
21
|
+
const lang = useLang();
|
|
22
|
+
const [ownIndex, setOwnIndex] = useState(defaultActiveIndex);
|
|
23
|
+
const [cycle, setCycle] = useState(0);
|
|
24
|
+
if (pages.length === 0) {
|
|
25
|
+
return _jsx(StoriesFrame, {
|
|
26
|
+
"$width": width,
|
|
27
|
+
children: _jsx(StoriesCanvas, {
|
|
28
|
+
children: _jsxs(EmptyState, {
|
|
29
|
+
children: [_jsx(ImageIcon, {
|
|
30
|
+
size: "medium",
|
|
31
|
+
view: "lined"
|
|
32
|
+
}), _jsx(Lang, {
|
|
33
|
+
message: "DUMB.PUSH_STORIES_PREVIEW.EMPTY"
|
|
34
|
+
})]
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const index = clampStoryIndex(activeIndex ?? ownIndex, pages.length);
|
|
40
|
+
const goNext = () => {
|
|
41
|
+
const next = (index + 1) % pages.length;
|
|
42
|
+
setCycle(value => value + 1);
|
|
43
|
+
if (activeIndex === undefined) {
|
|
44
|
+
setOwnIndex(next);
|
|
45
|
+
}
|
|
46
|
+
if (next !== index) {
|
|
47
|
+
onActiveIndexChange?.(next);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return _jsx(StoriesFrame, {
|
|
51
|
+
"$width": width,
|
|
52
|
+
children: _jsxs(StoriesCanvas, {
|
|
53
|
+
children: [_jsx(PushStoriesPage, {
|
|
54
|
+
page: pages[index]
|
|
55
|
+
}), _jsx(PushStoriesProgress, {
|
|
56
|
+
count: pages.length,
|
|
57
|
+
activeIndex: index,
|
|
58
|
+
playing: autoplay,
|
|
59
|
+
duration: normalizeStoryDuration(pages[index].duration),
|
|
60
|
+
cycle: cycle,
|
|
61
|
+
onComplete: goNext
|
|
62
|
+
}), interactive && _jsx(TapZone, {
|
|
63
|
+
"aria-label": lang({
|
|
64
|
+
message: 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE'
|
|
65
|
+
}),
|
|
66
|
+
type: "button",
|
|
67
|
+
onClick: goNext
|
|
68
|
+
})]
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { hasStoryButton, normalizeStoryDuration } from '../helpers';
|
|
4
|
+
import { PushStoriesPreview } from '../PushStoriesPreview';
|
|
5
|
+
const pages = [{
|
|
6
|
+
image: 'https://example.com/1.jpg',
|
|
7
|
+
title: 'Welcome',
|
|
8
|
+
subtitle: 'Swipe to explore',
|
|
9
|
+
buttonTitle: 'Get started',
|
|
10
|
+
link: 'app://one'
|
|
11
|
+
}, {
|
|
12
|
+
image: 'https://example.com/2.jpg',
|
|
13
|
+
title: 'Stay in the loop',
|
|
14
|
+
duration: 4
|
|
15
|
+
}, {
|
|
16
|
+
image: 'https://example.com/3.jpg',
|
|
17
|
+
title: 'You are all set',
|
|
18
|
+
buttonTitle: 'Open app',
|
|
19
|
+
link: 'app://three'
|
|
20
|
+
}];
|
|
21
|
+
const next = () => {
|
|
22
|
+
fireEvent.click(screen.getByLabelText('Next page'));
|
|
23
|
+
};
|
|
24
|
+
const progressFill = index => screen.getByTestId('push-stories-progress').children[index].firstElementChild;
|
|
25
|
+
describe('normalizeStoryDuration', () => {
|
|
26
|
+
it('plays a missing, zero, negative or non-finite duration for 5 seconds', () => {
|
|
27
|
+
expect(normalizeStoryDuration(undefined)).toBe(5);
|
|
28
|
+
expect(normalizeStoryDuration(0)).toBe(5);
|
|
29
|
+
expect(normalizeStoryDuration(-3)).toBe(5);
|
|
30
|
+
expect(normalizeStoryDuration(Number.NaN)).toBe(5);
|
|
31
|
+
expect(normalizeStoryDuration(Number.POSITIVE_INFINITY)).toBe(5);
|
|
32
|
+
});
|
|
33
|
+
it('keeps a valid duration and caps it at 30 seconds', () => {
|
|
34
|
+
expect(normalizeStoryDuration(1.5)).toBe(1.5);
|
|
35
|
+
expect(normalizeStoryDuration(45)).toBe(30);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('hasStoryButton', () => {
|
|
39
|
+
it('needs both a title and a link with a scheme, as the SDK does', () => {
|
|
40
|
+
expect(hasStoryButton({
|
|
41
|
+
buttonTitle: 'Go',
|
|
42
|
+
link: 'app://x'
|
|
43
|
+
})).toBe(true);
|
|
44
|
+
expect(hasStoryButton({
|
|
45
|
+
buttonTitle: 'Go',
|
|
46
|
+
link: 'https://example.com'
|
|
47
|
+
})).toBe(true);
|
|
48
|
+
expect(hasStoryButton({
|
|
49
|
+
buttonTitle: 'Go',
|
|
50
|
+
link: 'example.com/promo'
|
|
51
|
+
})).toBe(false);
|
|
52
|
+
expect(hasStoryButton({
|
|
53
|
+
buttonTitle: 'Go'
|
|
54
|
+
})).toBe(false);
|
|
55
|
+
expect(hasStoryButton({
|
|
56
|
+
buttonTitle: ' ',
|
|
57
|
+
link: 'app://x'
|
|
58
|
+
})).toBe(false);
|
|
59
|
+
expect(hasStoryButton({
|
|
60
|
+
link: 'app://x'
|
|
61
|
+
})).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('PushStoriesPreview', () => {
|
|
65
|
+
it('draws the empty state without pages', () => {
|
|
66
|
+
render(_jsx(PushStoriesPreview, {
|
|
67
|
+
pages: []
|
|
68
|
+
}));
|
|
69
|
+
expect(screen.getByText('Add a page to see the story preview')).toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByTestId('push-stories-progress')).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('draws one progress segment per page and the first page text', () => {
|
|
73
|
+
render(_jsx(PushStoriesPreview, {
|
|
74
|
+
pages: pages
|
|
75
|
+
}));
|
|
76
|
+
expect(screen.getByTestId('push-stories-progress').children).toHaveLength(3);
|
|
77
|
+
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('Swipe to explore')).toBeInTheDocument();
|
|
79
|
+
expect(screen.getByText('Get started')).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
it('keeps one segment per page and one image on screen through clicks and page list changes', () => {
|
|
82
|
+
const {
|
|
83
|
+
container,
|
|
84
|
+
rerender
|
|
85
|
+
} = render(_jsx(PushStoriesPreview, {
|
|
86
|
+
pages: pages
|
|
87
|
+
}));
|
|
88
|
+
const expectShape = count => {
|
|
89
|
+
expect(screen.getByTestId('push-stories-progress').children).toHaveLength(count);
|
|
90
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
91
|
+
};
|
|
92
|
+
for (let click = 0; click < 7; click += 1) {
|
|
93
|
+
expectShape(3);
|
|
94
|
+
next();
|
|
95
|
+
}
|
|
96
|
+
rerender(_jsx(PushStoriesPreview, {
|
|
97
|
+
pages: [...pages, {
|
|
98
|
+
image: 'https://example.com/4.jpg',
|
|
99
|
+
title: 'Fourth'
|
|
100
|
+
}]
|
|
101
|
+
}));
|
|
102
|
+
expectShape(4);
|
|
103
|
+
rerender(_jsx(PushStoriesPreview, {
|
|
104
|
+
pages: pages.slice(0, 2)
|
|
105
|
+
}));
|
|
106
|
+
expectShape(2);
|
|
107
|
+
});
|
|
108
|
+
it('hides the button of a page without a link', () => {
|
|
109
|
+
render(_jsx(PushStoriesPreview, {
|
|
110
|
+
pages: [{
|
|
111
|
+
...pages[0],
|
|
112
|
+
link: ''
|
|
113
|
+
}]
|
|
114
|
+
}));
|
|
115
|
+
expect(screen.queryByText('Get started')).not.toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
it('shows the next page on every click and goes round after the last one', () => {
|
|
118
|
+
render(_jsx(PushStoriesPreview, {
|
|
119
|
+
pages: pages
|
|
120
|
+
}));
|
|
121
|
+
next();
|
|
122
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
123
|
+
expect(progressFill(0)).not.toBeNull();
|
|
124
|
+
next();
|
|
125
|
+
expect(screen.getByText('You are all set')).toBeInTheDocument();
|
|
126
|
+
next();
|
|
127
|
+
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
|
128
|
+
expect(progressFill(1)).toBeNull();
|
|
129
|
+
next();
|
|
130
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
it('has a single click target over the whole story', () => {
|
|
133
|
+
render(_jsx(PushStoriesPreview, {
|
|
134
|
+
pages: pages
|
|
135
|
+
}));
|
|
136
|
+
expect(screen.getAllByRole('button')).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
it('follows the activeIndex prop and reports clicks instead of moving itself', () => {
|
|
139
|
+
const onActiveIndexChange = jest.fn();
|
|
140
|
+
const {
|
|
141
|
+
rerender
|
|
142
|
+
} = render(_jsx(PushStoriesPreview, {
|
|
143
|
+
pages: pages,
|
|
144
|
+
activeIndex: 1,
|
|
145
|
+
onActiveIndexChange: onActiveIndexChange
|
|
146
|
+
}));
|
|
147
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
148
|
+
next();
|
|
149
|
+
expect(onActiveIndexChange).toHaveBeenCalledWith(2);
|
|
150
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
151
|
+
rerender(_jsx(PushStoriesPreview, {
|
|
152
|
+
pages: pages,
|
|
153
|
+
activeIndex: 2,
|
|
154
|
+
onActiveIndexChange: onActiveIndexChange
|
|
155
|
+
}));
|
|
156
|
+
expect(screen.getByText('You are all set')).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
it('clamps an activeIndex past the last page', () => {
|
|
159
|
+
render(_jsx(PushStoriesPreview, {
|
|
160
|
+
pages: pages,
|
|
161
|
+
activeIndex: 9
|
|
162
|
+
}));
|
|
163
|
+
expect(screen.getByText('You are all set')).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
it('advances when the autoplay segment finishes and goes round after the last page', () => {
|
|
166
|
+
render(_jsx(PushStoriesPreview, {
|
|
167
|
+
pages: pages.slice(0, 2),
|
|
168
|
+
autoplay: true
|
|
169
|
+
}));
|
|
170
|
+
fireEvent.animationEnd(progressFill(0));
|
|
171
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
172
|
+
fireEvent.animationEnd(progressFill(1));
|
|
173
|
+
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
|
174
|
+
expect(progressFill(1)).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
it('fills segments up to and including the current page without autoplay', () => {
|
|
177
|
+
const {
|
|
178
|
+
container
|
|
179
|
+
} = render(_jsx(PushStoriesPreview, {
|
|
180
|
+
pages: pages
|
|
181
|
+
}));
|
|
182
|
+
const filled = () => [...screen.getByTestId('push-stories-progress').children].filter(track => track.firstElementChild).length;
|
|
183
|
+
expect(filled()).toBe(1);
|
|
184
|
+
next();
|
|
185
|
+
expect(filled()).toBe(2);
|
|
186
|
+
next();
|
|
187
|
+
expect(filled()).toBe(3);
|
|
188
|
+
next();
|
|
189
|
+
expect(filled()).toBe(1);
|
|
190
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
191
|
+
});
|
|
192
|
+
it('starts the next segment from zero when a click interrupts autoplay', () => {
|
|
193
|
+
render(_jsx(PushStoriesPreview, {
|
|
194
|
+
pages: pages,
|
|
195
|
+
autoplay: true
|
|
196
|
+
}));
|
|
197
|
+
const interrupted = progressFill(0);
|
|
198
|
+
next();
|
|
199
|
+
expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
|
|
200
|
+
expect(progressFill(0)).not.toBe(interrupted);
|
|
201
|
+
expect(progressFill(1)).not.toBeNull();
|
|
202
|
+
});
|
|
203
|
+
it('replays a single page instead of moving anywhere', () => {
|
|
204
|
+
render(_jsx(PushStoriesPreview, {
|
|
205
|
+
pages: pages.slice(0, 1),
|
|
206
|
+
autoplay: true
|
|
207
|
+
}));
|
|
208
|
+
const run = progressFill(0);
|
|
209
|
+
next();
|
|
210
|
+
expect(screen.getByTestId('push-stories-progress').children).toHaveLength(1);
|
|
211
|
+
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
|
212
|
+
expect(progressFill(0)).not.toBe(run);
|
|
213
|
+
});
|
|
214
|
+
it('draws a placeholder for a missing or broken image', () => {
|
|
215
|
+
const {
|
|
216
|
+
container
|
|
217
|
+
} = render(_jsx(PushStoriesPreview, {
|
|
218
|
+
pages: [{
|
|
219
|
+
title: 'No image'
|
|
220
|
+
}, pages[1]]
|
|
221
|
+
}));
|
|
222
|
+
expect(screen.getByTestId('push-stories-image-placeholder')).toBeInTheDocument();
|
|
223
|
+
next();
|
|
224
|
+
fireEvent.error(container.querySelector('img'));
|
|
225
|
+
expect(screen.getByTestId('push-stories-image-placeholder')).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
it('drops the click target when not interactive', () => {
|
|
228
|
+
render(_jsx(PushStoriesPreview, {
|
|
229
|
+
pages: pages,
|
|
230
|
+
interactive: false
|
|
231
|
+
}));
|
|
232
|
+
expect(screen.queryByLabelText('Next page')).not.toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { PushStoriesPageProps } from './types';
|
|
3
|
+
/** One story page: the image, the scrim and the title, subtitle and button laid over it. */
|
|
4
|
+
export declare function PushStoriesPage(props: PushStoriesPageProps): ReactElement;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ImageIcon } from '@pushwoosh/kit-icons';
|
|
4
|
+
import { BottomArea, ImagePlaceholder, PageButton, PageButtonLabel, PageImage, PageSubtitle, PageTitle } from './styles';
|
|
5
|
+
import { hasStoryButton } from '../../helpers';
|
|
6
|
+
/** One story page: the image, the scrim and the title, subtitle and button laid over it. */
|
|
7
|
+
export function PushStoriesPage(props) {
|
|
8
|
+
const {
|
|
9
|
+
page
|
|
10
|
+
} = props;
|
|
11
|
+
const [failedImage, setFailedImage] = useState();
|
|
12
|
+
const image = page.image?.trim();
|
|
13
|
+
const title = page.title?.trim();
|
|
14
|
+
const subtitle = page.subtitle?.trim();
|
|
15
|
+
return _jsxs(_Fragment, {
|
|
16
|
+
children: [image && failedImage !== image ? _jsx(PageImage, {
|
|
17
|
+
alt: "",
|
|
18
|
+
src: image,
|
|
19
|
+
onError: () => setFailedImage(image)
|
|
20
|
+
}) : _jsx(ImagePlaceholder, {
|
|
21
|
+
"data-testid": "push-stories-image-placeholder",
|
|
22
|
+
children: _jsx(ImageIcon, {
|
|
23
|
+
size: "medium",
|
|
24
|
+
view: "lined"
|
|
25
|
+
})
|
|
26
|
+
}), _jsxs(BottomArea, {
|
|
27
|
+
children: [title && _jsx(PageTitle, {
|
|
28
|
+
children: title
|
|
29
|
+
}), subtitle && _jsx(PageSubtitle, {
|
|
30
|
+
children: subtitle
|
|
31
|
+
}), hasStoryButton(page) && _jsx(PageButton, {
|
|
32
|
+
children: _jsx(PageButtonLabel, {
|
|
33
|
+
children: page.buttonTitle?.trim()
|
|
34
|
+
})
|
|
35
|
+
})]
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PushStoriesPage } from './PushStoriesPage';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PushStoriesPage } from './PushStoriesPage';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const PageImage: import("styled-components").StyledComponent<"img", any, {}, never>;
|
|
2
|
+
export declare const ImagePlaceholder: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
3
|
+
export declare const BottomArea: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
4
|
+
export declare const PageTitle: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
5
|
+
export declare const PageSubtitle: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
6
|
+
export declare const PageButton: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
7
|
+
export declare const PageButtonLabel: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { PLACEHOLDER_BACKGROUND } from '../../constants';
|
|
3
|
+
import { scaled } from '../../helpers';
|
|
4
|
+
export const PageImage = styled.img.withConfig({
|
|
5
|
+
displayName: "PageImage",
|
|
6
|
+
componentId: "sc-1q2ujdq-0"
|
|
7
|
+
})(["position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"]);
|
|
8
|
+
export const ImagePlaceholder = styled.div.withConfig({
|
|
9
|
+
displayName: "ImagePlaceholder",
|
|
10
|
+
componentId: "sc-1q2ujdq-1"
|
|
11
|
+
})(["position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:", ";color:rgba(255,255,255,0.45);"], PLACEHOLDER_BACKGROUND);
|
|
12
|
+
export const BottomArea = styled.div.withConfig({
|
|
13
|
+
displayName: "BottomArea",
|
|
14
|
+
componentId: "sc-1q2ujdq-2"
|
|
15
|
+
})(["position:absolute;left:0;right:0;bottom:0;z-index:2;display:flex;flex-direction:column;padding:", " ", " ", ";background:linear-gradient(to bottom,rgba(0,0,0,0) 0%,rgba(0,0,0,0.75) 100%);pointer-events:none;"], scaled(44), scaled(24), scaled(24));
|
|
16
|
+
export const PageTitle = styled.div.withConfig({
|
|
17
|
+
displayName: "PageTitle",
|
|
18
|
+
componentId: "sc-1q2ujdq-3"
|
|
19
|
+
})(["display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;color:#FFF;font-size:", ";font-weight:700;line-height:1.2;overflow-wrap:anywhere;"], scaled(21));
|
|
20
|
+
export const PageSubtitle = styled.div.withConfig({
|
|
21
|
+
displayName: "PageSubtitle",
|
|
22
|
+
componentId: "sc-1q2ujdq-4"
|
|
23
|
+
})(["display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;color:rgba(255,255,255,0.85);font-size:", ";font-weight:400;line-height:1.3;overflow-wrap:anywhere;* + &{margin-top:", ";}"], scaled(14), scaled(6));
|
|
24
|
+
export const PageButton = styled.div.withConfig({
|
|
25
|
+
displayName: "PageButton",
|
|
26
|
+
componentId: "sc-1q2ujdq-5"
|
|
27
|
+
})(["display:flex;align-items:center;justify-content:center;height:", ";padding:0 ", ";border-radius:", ";background:#FFF;box-shadow:0 ", " ", " rgba(0,0,0,0.28);color:#0F0F0F;font-size:", ";font-weight:600;* + &{margin-top:", ";}"], scaled(52), scaled(28), scaled(26), scaled(6), scaled(28), scaled(17), scaled(16));
|
|
28
|
+
export const PageButtonLabel = styled.span.withConfig({
|
|
29
|
+
displayName: "PageButtonLabel",
|
|
30
|
+
componentId: "sc-1q2ujdq-6"
|
|
31
|
+
})(["overflow:hidden;white-space:nowrap;text-overflow:ellipsis;"]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ProgressFill, ProgressRow, ProgressTrack } from './styles';
|
|
3
|
+
/** Segmented bar over the top of the story, one segment per page. */
|
|
4
|
+
export function PushStoriesProgress(props) {
|
|
5
|
+
const {
|
|
6
|
+
count,
|
|
7
|
+
activeIndex,
|
|
8
|
+
playing,
|
|
9
|
+
duration,
|
|
10
|
+
cycle,
|
|
11
|
+
onComplete
|
|
12
|
+
} = props;
|
|
13
|
+
return _jsx(ProgressRow, {
|
|
14
|
+
"data-testid": "push-stories-progress",
|
|
15
|
+
children: Array.from({
|
|
16
|
+
length: count
|
|
17
|
+
}, (_, index) => _jsxs(ProgressTrack, {
|
|
18
|
+
children: [(index < activeIndex || index === activeIndex && !playing) && _jsx(ProgressFill, {}), index === activeIndex && playing && _jsx(ProgressFill, {
|
|
19
|
+
"$duration": duration,
|
|
20
|
+
onAnimationEnd: onComplete
|
|
21
|
+
}, cycle)]
|
|
22
|
+
}, index))
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PushStoriesProgress } from './PushStoriesProgress';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PushStoriesProgress } from './PushStoriesProgress';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const ProgressRow: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
2
|
+
export declare const ProgressTrack: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
3
|
+
export declare const ProgressFill: import("styled-components").StyledComponent<"div", any, {
|
|
4
|
+
$duration?: number;
|
|
5
|
+
}, never>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import styled, { css, keyframes } from 'styled-components';
|
|
2
|
+
import { scaled } from '../../helpers';
|
|
3
|
+
const grow = keyframes(["from{width:0;}to{width:100%;}"]);
|
|
4
|
+
export const ProgressRow = styled.div.withConfig({
|
|
5
|
+
displayName: "ProgressRow",
|
|
6
|
+
componentId: "sc-1p5nb36-0"
|
|
7
|
+
})(["position:absolute;top:", ";left:", ";right:", ";z-index:2;display:flex;gap:", ";pointer-events:none;"], scaled(23), scaled(12), scaled(12), scaled(4));
|
|
8
|
+
export const ProgressTrack = styled.div.withConfig({
|
|
9
|
+
displayName: "ProgressTrack",
|
|
10
|
+
componentId: "sc-1p5nb36-1"
|
|
11
|
+
})(["flex:1;height:max(2px,", ");border-radius:", ";background:rgba(255,255,255,0.3);overflow:hidden;"], scaled(3), scaled(1.5));
|
|
12
|
+
export const ProgressFill = styled.div.withConfig({
|
|
13
|
+
displayName: "ProgressFill",
|
|
14
|
+
componentId: "sc-1p5nb36-2"
|
|
15
|
+
})(["width:100%;height:100%;background:#FFF;", ""], ({
|
|
16
|
+
$duration
|
|
17
|
+
}) => $duration !== undefined && css(["animation:", " ", "s linear both;"], grow, $duration));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Props of {@link PushStoriesProgress}. */
|
|
2
|
+
export interface PushStoriesProgressProps {
|
|
3
|
+
/** Number of pages, one segment each. */
|
|
4
|
+
count: number;
|
|
5
|
+
/** Segment of the page on screen. */
|
|
6
|
+
activeIndex: number;
|
|
7
|
+
/** Fill the active segment over `duration`; otherwise it is drawn full, so page N shows N full segments. */
|
|
8
|
+
playing: boolean;
|
|
9
|
+
/** Seconds the active segment takes to fill. */
|
|
10
|
+
duration: number;
|
|
11
|
+
/** Changes whenever playback of the active page restarts, even on the same index. */
|
|
12
|
+
cycle: number;
|
|
13
|
+
/** Called when the active segment finishes filling. */
|
|
14
|
+
onComplete: () => void;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Height ÷ width of the stories area, the iOS SDK's default `storyAspectRatio`. */
|
|
2
|
+
export declare const PUSH_STORIES_ASPECT_RATIO = 1.5;
|
|
3
|
+
/** Seconds a page stays on screen when its duration is missing or invalid. */
|
|
4
|
+
export declare const PUSH_STORIES_DEFAULT_DURATION = 5;
|
|
5
|
+
/** Longest page duration in seconds the iOS SDK honours. */
|
|
6
|
+
export declare const PUSH_STORIES_MAX_DURATION = 30;
|
|
7
|
+
export declare const REFERENCE_WIDTH = 375;
|
|
8
|
+
export declare const PLACEHOLDER_BACKGROUND = "linear-gradient(160deg, #3A3A3C 0%, #1C1C1E 100%)";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Height ÷ width of the stories area, the iOS SDK's default `storyAspectRatio`. */
|
|
2
|
+
export const PUSH_STORIES_ASPECT_RATIO = 1.5;
|
|
3
|
+
/** Seconds a page stays on screen when its duration is missing or invalid. */
|
|
4
|
+
export const PUSH_STORIES_DEFAULT_DURATION = 5;
|
|
5
|
+
/** Longest page duration in seconds the iOS SDK honours. */
|
|
6
|
+
export const PUSH_STORIES_MAX_DURATION = 30;
|
|
7
|
+
export const REFERENCE_WIDTH = 375;
|
|
8
|
+
export const PLACEHOLDER_BACKGROUND = 'linear-gradient(160deg, #3A3A3C 0%, #1C1C1E 100%)';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PushStoriesPreviewPage } from './types';
|
|
2
|
+
/** Page duration in seconds as the iOS SDK plays it. */
|
|
3
|
+
export declare function normalizeStoryDuration(duration?: number): number;
|
|
4
|
+
/** Whether the iOS SDK draws the page's button: it needs a title and a link with a scheme. */
|
|
5
|
+
export declare function hasStoryButton(page: PushStoriesPreviewPage): boolean;
|
|
6
|
+
export declare function clampStoryIndex(index: number, length: number): number;
|
|
7
|
+
export declare function scaled(points: number): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, REFERENCE_WIDTH } from './constants';
|
|
2
|
+
const URL_SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
3
|
+
/** Page duration in seconds as the iOS SDK plays it. */
|
|
4
|
+
export function normalizeStoryDuration(duration) {
|
|
5
|
+
if (typeof duration !== 'number' || !Number.isFinite(duration) || duration <= 0) {
|
|
6
|
+
return PUSH_STORIES_DEFAULT_DURATION;
|
|
7
|
+
}
|
|
8
|
+
return Math.min(duration, PUSH_STORIES_MAX_DURATION);
|
|
9
|
+
}
|
|
10
|
+
/** Whether the iOS SDK draws the page's button: it needs a title and a link with a scheme. */
|
|
11
|
+
export function hasStoryButton(page) {
|
|
12
|
+
return !!page.buttonTitle?.trim() && URL_SCHEME.test(page.link?.trim() || '');
|
|
13
|
+
}
|
|
14
|
+
export function clampStoryIndex(index, length) {
|
|
15
|
+
return Math.max(0, Math.min(index, length - 1));
|
|
16
|
+
}
|
|
17
|
+
export function scaled(points) {
|
|
18
|
+
return `calc(${points} * 100cqw / ${REFERENCE_WIDTH})`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PushStoriesPreview } from './PushStoriesPreview';
|
|
2
|
+
export { hasStoryButton, normalizeStoryDuration } from './helpers';
|
|
3
|
+
export { PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, } from './constants';
|
|
4
|
+
export type { PushStoriesPreviewPage, PushStoriesPreviewProps } from './types';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { messages as ar } from './messages_ar';
|
|
2
|
+
import { messages as bn } from './messages_bn';
|
|
3
|
+
import { messages as de } from './messages_de';
|
|
4
|
+
import { messages as en } from './messages_en';
|
|
5
|
+
import { messages as es } from './messages_es';
|
|
6
|
+
import { messages as fr } from './messages_fr';
|
|
7
|
+
import { messages as he } from './messages_he';
|
|
8
|
+
import { messages as hi } from './messages_hi';
|
|
9
|
+
import { messages as ja } from './messages_ja';
|
|
10
|
+
import { messages as ko } from './messages_ko';
|
|
11
|
+
import { messages as pt } from './messages_pt';
|
|
12
|
+
import { messages as ru } from './messages_ru';
|
|
13
|
+
import { messages as th } from './messages_th';
|
|
14
|
+
import { messages as zh } from './messages_zh';
|
|
15
|
+
import { createLang } from '../../polyglot';
|
|
16
|
+
export const {
|
|
17
|
+
Lang,
|
|
18
|
+
useLang
|
|
19
|
+
} = createLang({
|
|
20
|
+
ar,
|
|
21
|
+
bn,
|
|
22
|
+
de,
|
|
23
|
+
en,
|
|
24
|
+
es,
|
|
25
|
+
fr,
|
|
26
|
+
he,
|
|
27
|
+
hi,
|
|
28
|
+
ja,
|
|
29
|
+
ko,
|
|
30
|
+
pt,
|
|
31
|
+
ru,
|
|
32
|
+
th,
|
|
33
|
+
zh
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const StoriesFrame: import("styled-components").StyledComponent<"div", any, {
|
|
2
|
+
$width?: string | number;
|
|
3
|
+
}, never>;
|
|
4
|
+
export declare const StoriesCanvas: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
5
|
+
export declare const TapZone: import("styled-components").StyledComponent<"button", any, {}, never>;
|
|
6
|
+
export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { FontFamily } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { toCssValue } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { PLACEHOLDER_BACKGROUND, PUSH_STORIES_ASPECT_RATIO } from './constants';
|
|
5
|
+
import { scaled } from './helpers';
|
|
6
|
+
export const StoriesFrame = styled.div.withConfig({
|
|
7
|
+
displayName: "StoriesFrame",
|
|
8
|
+
componentId: "sc-udrlhm-0"
|
|
9
|
+
})(["position:relative;container-type:inline-size;width:", ";aspect-ratio:1 / ", ";font-family:", ";user-select:none;"], ({
|
|
10
|
+
$width
|
|
11
|
+
}) => $width ? toCssValue($width) : '100%', PUSH_STORIES_ASPECT_RATIO, FontFamily.MAIN);
|
|
12
|
+
export const StoriesCanvas = styled.div.withConfig({
|
|
13
|
+
displayName: "StoriesCanvas",
|
|
14
|
+
componentId: "sc-udrlhm-1"
|
|
15
|
+
})(["position:absolute;inset:0;overflow:hidden;border-radius:", ";background:#000;isolation:isolate;"], scaled(24));
|
|
16
|
+
export const TapZone = styled.button.withConfig({
|
|
17
|
+
displayName: "TapZone",
|
|
18
|
+
componentId: "sc-udrlhm-2"
|
|
19
|
+
})(["position:absolute;inset:0;z-index:1;padding:0;border:none;background:transparent;cursor:pointer;&:focus-visible{outline:none;box-shadow:inset 0 0 0 2px rgba(255,255,255,0.6);}"]);
|
|
20
|
+
export const EmptyState = styled.div.withConfig({
|
|
21
|
+
displayName: "EmptyState",
|
|
22
|
+
componentId: "sc-udrlhm-3"
|
|
23
|
+
})(["position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:", ";padding:0 ", ";background:", ";color:rgba(255,255,255,0.7);font-size:", ";line-height:1.35;text-align:center;"], scaled(12), scaled(32), PLACEHOLDER_BACKGROUND, scaled(15));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One full-screen page of an iOS push story, in the shape the push preset form edits. */
|
|
2
|
+
export interface PushStoriesPreviewPage {
|
|
3
|
+
/** Full-bleed image URL; an empty or broken one is drawn as a placeholder. */
|
|
4
|
+
image?: string;
|
|
5
|
+
/** Seconds before the page auto-advances; clamped to 30, non-positive falls back to 5. */
|
|
6
|
+
duration?: number;
|
|
7
|
+
/** Title over the bottom of the page, two lines at most. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Subtitle under the title, two lines at most. */
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
/** Call-to-action text; the button shows only together with a `link` that has a scheme. */
|
|
12
|
+
buttonTitle?: string;
|
|
13
|
+
/** Deep link the button opens on the device. */
|
|
14
|
+
link?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Props of {@link PushStoriesPreview}. */
|
|
17
|
+
export interface PushStoriesPreviewProps {
|
|
18
|
+
/** Pages in playback order; an empty list draws the empty state. */
|
|
19
|
+
pages: PushStoriesPreviewPage[];
|
|
20
|
+
/** Page to show; pass it to control the preview, e.g. to follow the page being edited. */
|
|
21
|
+
activeIndex?: number;
|
|
22
|
+
/** Page shown first when `activeIndex` is not controlled (default 0). */
|
|
23
|
+
defaultActiveIndex?: number;
|
|
24
|
+
/** Called when a click or autoplay moves to another page. */
|
|
25
|
+
onActiveIndexChange?: (index: number) => void;
|
|
26
|
+
/** Advance pages by their `duration`, animating the progress bar, round and round (default false). */
|
|
27
|
+
autoplay?: boolean;
|
|
28
|
+
/** A click anywhere on the story shows the next page, after the last one the first (default true). */
|
|
29
|
+
interactive?: boolean;
|
|
30
|
+
/** CSS width; the height follows the device's 2:3 stories ratio (default 100%). */
|
|
31
|
+
width?: string | number;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export { Link, type LinkPropsWithoutAs, type LinkPropsWithAsTag, type LinkPropsW
|
|
|
34
34
|
export { StatisticCard } from './StatisticCard';
|
|
35
35
|
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, type EmailClickMapItem, type EmailClickMapPlacement, type EmailClickMapProps, type PreviewIframeProps, } from './EmailTemplatePreview';
|
|
36
36
|
export { PushPreview } from './PushPreview';
|
|
37
|
+
export { PushStoriesPreview, hasStoryButton, normalizeStoryDuration, PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, type PushStoriesPreviewPage, type PushStoriesPreviewProps, } from './PushStoriesPreview';
|
|
37
38
|
export { InboxPreview, resolveInboxLayout, MAX_CAROUSEL_SLIDES, type InboxLayoutType, type InboxPreviewPlatform, type InboxPreviewContent, type InboxPreviewProps, type InboxPreviewSlide, } from './InboxPreview';
|
|
38
39
|
export { WhatsAppPreview } from './WhatsAppPreview';
|
|
39
40
|
export type { WhatsAppAttachment, WhatsAppAttachmentKind, WhatsAppMessage, WhatsAppPreviewProps, } from './WhatsAppPreview';
|
package/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export { Link } from './Link';
|
|
|
34
34
|
export { StatisticCard } from './StatisticCard';
|
|
35
35
|
export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE } from './EmailTemplatePreview';
|
|
36
36
|
export { PushPreview } from './PushPreview';
|
|
37
|
+
export { PushStoriesPreview, hasStoryButton, normalizeStoryDuration, PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION } from './PushStoriesPreview';
|
|
37
38
|
export { InboxPreview, resolveInboxLayout, MAX_CAROUSEL_SLIDES } from './InboxPreview';
|
|
38
39
|
export { WhatsAppPreview } from './WhatsAppPreview';
|
|
39
40
|
export { Toast, ToastProvider, useToast } from './Toast';
|