@revenuecat/purchases-ui-js 4.13.0 → 4.14.1

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.
@@ -224,7 +224,12 @@
224
224
 
225
225
  let startTranslation = $state(0);
226
226
 
227
+ // Releasing after holding still this long is not a flick, however fast the drag was.
228
+ const FLICK_MAX_PAUSE_MS = 100;
229
+
227
230
  let lastClientX = 0;
231
+ let lastMoveTimeStamp = 0;
232
+ let velocityPxPerMs: number | null = null;
228
233
 
229
234
  function onpointerdown(event: PointerEvent) {
230
235
  const target = event.currentTarget as HTMLElement;
@@ -232,9 +237,21 @@
232
237
  startTranslation = getTranslation(slider);
233
238
  gestureStartPage = currentPage;
234
239
  lastClientX = event.clientX;
240
+ lastMoveTimeStamp = event.timeStamp;
241
+ velocityPxPerMs = null;
235
242
  }
236
243
 
237
244
  function onpointerup(event: PointerEvent) {
245
+ const stalled = event.timeStamp - lastMoveTimeStamp > FLICK_MAX_PAUSE_MS;
246
+ settleGesture(event, stalled ? 0 : (velocityPxPerMs ?? 0));
247
+ }
248
+
249
+ // Fires when the browser takes over the touch (e.g. vertical scroll).
250
+ function onpointercancel(event: PointerEvent) {
251
+ settleGesture(event, 0);
252
+ }
253
+
254
+ function settleGesture(event: PointerEvent, releaseVelocityPxPerMs: number) {
238
255
  const target = event.currentTarget as HTMLElement;
239
256
  if (target.hasPointerCapture(event.pointerId)) {
240
257
  target.releasePointerCapture(event.pointerId);
@@ -242,12 +259,10 @@
242
259
 
243
260
  const translation = getTranslation(slider);
244
261
 
245
- // Advance at most one page from where the gesture started, based on how
246
- // far the finger travelled relative to a page's width (see
247
- // getSwipeTargetPage).
248
262
  const page = getSwipeTargetPage({
249
263
  startPage: gestureStartPage ?? getPageFromTranslation(translation),
250
264
  dragDistance: translation - startTranslation,
265
+ velocityPxPerMs: releaseVelocityPxPerMs,
251
266
  pageWidth,
252
267
  pageCount: pages.length,
253
268
  loop: carouselPagingLoop,
@@ -284,7 +299,17 @@
284
299
  // for touch-derived pointer events in Safari and device-pixel scaled in
285
300
  // some Chrome configurations.
286
301
  const deltaX = event.clientX - lastClientX;
302
+ // At least 1ms: synthetic input and coarse timestamps can report 0ms.
303
+ const deltaT = Math.max(1, event.timeStamp - lastMoveTimeStamp);
287
304
  lastClientX = event.clientX;
305
+ lastMoveTimeStamp = event.timeStamp;
306
+ // Moving average, so one jittery pointermove doesn't decide the release
307
+ // speed; the first sample seeds it so short flicks aren't underestimated.
308
+ const sampleVelocity = deltaX / deltaT;
309
+ velocityPxPerMs =
310
+ velocityPxPerMs === null
311
+ ? sampleVelocity
312
+ : 0.6 * velocityPxPerMs + 0.4 * sampleVelocity;
288
313
 
289
314
  const [min, max] = carouselPagingLoop
290
315
  ? [startTranslation - carouselWidth, startTranslation + carouselWidth]
@@ -330,6 +355,7 @@
330
355
  {ontransitionend}
331
356
  {onpointerdown}
332
357
  {onpointerup}
358
+ {onpointercancel}
333
359
  {onpointermove}
334
360
  class="slider"
335
361
  style={sliderStyle}
@@ -7,35 +7,39 @@ export declare function clamp(value: number, min: number, max: number): number;
7
7
  * `Array.prototype.at` so Chrome < 92 does not throw while mounting.
8
8
  */
9
9
  export declare function getDefaultPageIndex<T>(pages: readonly T[], initialPageIndex: number): number;
10
+ /** Release speed (0.4 px/ms = 400 px/s) at or above which a swipe counts as a flick. */
11
+ export declare const FLICK_VELOCITY_PX_PER_MS = 0.4;
10
12
  /**
11
- * Advance one page when the drag passed this fraction of a page's width,
12
- * matching the iOS SDK carousel. A shorter drag snaps back to the page the
13
- * gesture started on.
14
- */
15
- export declare const PAGE_ADVANCE_THRESHOLD = 0.2;
16
- /**
17
- * Decides which page a swipe settles on when the pointer is released.
13
+ * Decides which page a swipe settles on when the pointer is released. Matches
14
+ * the native (iOS and Android) SDK carousels:
15
+ *
16
+ * - A flick (release speed >= `FLICK_VELOCITY_PX_PER_MS`) moves in the
17
+ * direction the pointer was travelling when released. From a partial drag it
18
+ * lands on the next page boundary that way, so dragging forward then flicking
19
+ * back returns to the start page.
20
+ * - A slower release settles on whichever page is nearest, i.e. the drag must
21
+ * pass `SLOW_DRAG_THRESHOLD` of a page's width to change page.
22
+ * - The result is never more than one page from `startPage`.
18
23
  *
19
- * Distance-based (not velocity-based), mirroring the iOS SDK's `handleDragEnd`:
20
- * a drag past `PAGE_ADVANCE_THRESHOLD` of a page's width advances exactly one
21
- * page in the swipe direction regardless of speed; a shorter drag stays put.
22
24
  * Non-loop carousels clamp at their first and last page; loop carousels may
23
25
  * return an index just outside `[0, pageCount)` so the caller can animate into
24
26
  * a clone before normalizing.
25
27
  *
26
- * @param startPage Page the gesture started on.
27
- * @param dragDistance Signed change in slider translation (negative = finger
28
- * moved left / next page).
29
- * @param pageWidth Width of a single page in px.
30
- * @param pageCount Total number of real pages.
31
- * @param loop Whether the carousel loops.
28
+ * @param startPage Page the gesture started on.
29
+ * @param dragDistance Signed change in slider translation in px (negative =
30
+ * pointer moved left / next page).
31
+ * @param velocityPxPerMs Signed pointer velocity at release, same sign
32
+ * convention as `dragDistance`.
33
+ * @param pageWidth Width of a single page in px.
34
+ * @param pageCount Total number of real pages.
35
+ * @param loop Whether the carousel loops.
32
36
  */
33
- export declare function getSwipeTargetPage({ startPage, dragDistance, pageWidth, pageCount, loop, advanceThreshold, }: {
37
+ export declare function getSwipeTargetPage({ startPage, dragDistance, velocityPxPerMs, pageWidth, pageCount, loop, }: {
34
38
  startPage: number;
35
39
  dragDistance: number;
40
+ velocityPxPerMs: number;
36
41
  pageWidth: number;
37
42
  pageCount: number;
38
43
  loop: boolean;
39
- advanceThreshold?: number;
40
44
  }): number;
41
45
  export declare function getTranslation(element: HTMLElement | null): number;
@@ -19,40 +19,54 @@ export function clamp(value, min, max) {
19
19
  export function getDefaultPageIndex(pages, initialPageIndex) {
20
20
  return pages[initialPageIndex] !== undefined ? initialPageIndex : 0;
21
21
  }
22
+ /** Fraction of a page's width a slow drag must pass to change page. */
23
+ const SLOW_DRAG_THRESHOLD = 0.5;
24
+ /** Release speed (0.4 px/ms = 400 px/s) at or above which a swipe counts as a flick. */
25
+ export const FLICK_VELOCITY_PX_PER_MS = 0.4;
22
26
  /**
23
- * Advance one page when the drag passed this fraction of a page's width,
24
- * matching the iOS SDK carousel. A shorter drag snaps back to the page the
25
- * gesture started on.
26
- */
27
- export const PAGE_ADVANCE_THRESHOLD = 0.2;
28
- /**
29
- * Decides which page a swipe settles on when the pointer is released.
27
+ * Decides which page a swipe settles on when the pointer is released. Matches
28
+ * the native (iOS and Android) SDK carousels:
29
+ *
30
+ * - A flick (release speed >= `FLICK_VELOCITY_PX_PER_MS`) moves in the
31
+ * direction the pointer was travelling when released. From a partial drag it
32
+ * lands on the next page boundary that way, so dragging forward then flicking
33
+ * back returns to the start page.
34
+ * - A slower release settles on whichever page is nearest, i.e. the drag must
35
+ * pass `SLOW_DRAG_THRESHOLD` of a page's width to change page.
36
+ * - The result is never more than one page from `startPage`.
30
37
  *
31
- * Distance-based (not velocity-based), mirroring the iOS SDK's `handleDragEnd`:
32
- * a drag past `PAGE_ADVANCE_THRESHOLD` of a page's width advances exactly one
33
- * page in the swipe direction regardless of speed; a shorter drag stays put.
34
38
  * Non-loop carousels clamp at their first and last page; loop carousels may
35
39
  * return an index just outside `[0, pageCount)` so the caller can animate into
36
40
  * a clone before normalizing.
37
41
  *
38
- * @param startPage Page the gesture started on.
39
- * @param dragDistance Signed change in slider translation (negative = finger
40
- * moved left / next page).
41
- * @param pageWidth Width of a single page in px.
42
- * @param pageCount Total number of real pages.
43
- * @param loop Whether the carousel loops.
42
+ * @param startPage Page the gesture started on.
43
+ * @param dragDistance Signed change in slider translation in px (negative =
44
+ * pointer moved left / next page).
45
+ * @param velocityPxPerMs Signed pointer velocity at release, same sign
46
+ * convention as `dragDistance`.
47
+ * @param pageWidth Width of a single page in px.
48
+ * @param pageCount Total number of real pages.
49
+ * @param loop Whether the carousel loops.
44
50
  */
45
- export function getSwipeTargetPage({ startPage, dragDistance, pageWidth, pageCount, loop, advanceThreshold = PAGE_ADVANCE_THRESHOLD, }) {
46
- const threshold = pageWidth * advanceThreshold;
47
- let page = startPage;
48
- if (dragDistance < -threshold) {
49
- // Finger moved left, content shifts left: next page.
50
- page += 1;
51
+ export function getSwipeTargetPage({ startPage, dragDistance, velocityPxPerMs, pageWidth, pageCount, loop, }) {
52
+ // Pages travelled from startPage; positive = towards the next page.
53
+ const offset = -dragDistance / pageWidth;
54
+ // A flick lands on the next whole page past the current offset in its
55
+ // direction; from a page-aligned start that is the adjacent page.
56
+ let pageDelta;
57
+ if (velocityPxPerMs <= -FLICK_VELOCITY_PX_PER_MS) {
58
+ pageDelta = Math.floor(offset) + 1;
59
+ }
60
+ else if (velocityPxPerMs >= FLICK_VELOCITY_PX_PER_MS) {
61
+ pageDelta = Math.ceil(offset) - 1;
62
+ }
63
+ else if (Math.abs(offset) > SLOW_DRAG_THRESHOLD) {
64
+ pageDelta = Math.sign(offset);
51
65
  }
52
- else if (dragDistance > threshold) {
53
- // Finger moved right: previous page.
54
- page -= 1;
66
+ else {
67
+ pageDelta = 0;
55
68
  }
69
+ const page = startPage + clamp(pageDelta, -1, 1);
56
70
  return loop ? page : clamp(page, 0, pageCount - 1);
57
71
  }
58
72
  export function getTranslation(element) {
@@ -136,6 +136,11 @@ export function getHtmlFromMarkdown(text) {
136
136
  bold: { regexp: /\*\*(.*?)\*\*/g, output: "<b>$1</b>" },
137
137
  italic: { regexp: /\*(.*?)\*/g, output: "<i>$1</i>" },
138
138
  strikethrough: { regexp: /~(.*?)~/g, output: "<s>$1</s>" },
139
+ // escapeHtml turned `<u>` tags into `&lt;u&gt;`, so we convert them back:
140
+ underline: { regexp: /&lt;u&gt;(.*?)&lt;\/u&gt;/g, output: "<u>$1</u>" },
141
+ // NOTE: Existing bug: the dashboard saves a literal "<" as `&lt;` and
142
+ // escapeHtml escapes it again, so "Show your support <3" shows as
143
+ // "Show your support &lt;3".
139
144
  code: {
140
145
  regexp: /`(.*?)`/g,
141
146
  output: "<span style='font-family: monospace'>$1</span>",
@@ -5388,7 +5388,7 @@ export const fontsPaywallData = {
5388
5388
  ptR0PssHwi: "Heading L",
5389
5389
  wGpryncQ4j: "Semibold",
5390
5390
  zZXnBwaz8X: "Heading S",
5391
- uzD2rPJ68m: "**this is bold text**\\\n*this is italics*\\\n~this is strikethrough~\\\n`this is code`\\\n[this is a link](https://www.google.com)",
5391
+ uzD2rPJ68m: "**this is bold text**\\\n*this is italics*\\\n~this is strikethrough~\\\n<u>this is underlined</u>\\\n`this is code`\\\n[this is a link](https://www.google.com)",
5392
5392
  },
5393
5393
  },
5394
5394
  created_at: "2024-11-08T21:39:13Z",
@@ -6443,7 +6443,7 @@ export const customFontsPaywallData = {
6443
6443
  ptR0PssHwi: "Heading L",
6444
6444
  wGpryncQ4j: "Semibold",
6445
6445
  zZXnBwaz8X: "Heading S",
6446
- uzD2rPJ68m: "**this is bold text**\\\n*this is italics*\\\n~this is strikethrough~\\\n`this is code`\\\n[this is a link](https://www.google.com)",
6446
+ uzD2rPJ68m: "**this is bold text**\\\n*this is italics*\\\n~this is strikethrough~\\\n<u>this is underlined</u>\\\n`this is code`\\\n[this is a link](https://www.google.com)",
6447
6447
  },
6448
6448
  },
6449
6449
  created_at: "2024-11-08T21:39:13Z",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@revenuecat/purchases-ui-js",
3
3
  "description": "Web components for Paywalls. Powered by RevenueCat",
4
4
  "private": false,
5
- "version": "4.13.0",
5
+ "version": "4.14.1",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },