|
1 | 1 | import { render } from 'preact' |
2 | 2 | import { PostHog } from '../../posthog-core' |
3 | | -import { ProductTour, ProductTourDismissReason, ProductTourRenderReason } from '../../posthog-product-tours-types' |
4 | | -import { checkTourConditions } from '../../utils/product-tour-utils' |
| 3 | +import { |
| 4 | + ProductTour, |
| 5 | + ProductTourCallback, |
| 6 | + ProductTourDismissReason, |
| 7 | + ProductTourRenderReason, |
| 8 | +} from '../../posthog-product-tours-types' |
5 | 9 | import { findElementBySelector, getElementMetadata, getProductTourStylesheet } from './product-tours-utils' |
6 | 10 | import { ProductTourTooltip } from './components/ProductTourTooltip' |
7 | 11 | import { createLogger } from '../../utils/logger' |
8 | | -import { document as _document } from '../../utils/globals' |
| 12 | +import { document as _document, window as _window } from '../../utils/globals' |
9 | 13 | import { localStore } from '../../storage' |
10 | 14 | import { addEventListener } from '../../utils' |
| 15 | +import { isNull } from '@posthog/core' |
11 | 16 |
|
12 | 17 | const logger = createLogger('[Product Tours]') |
13 | 18 |
|
14 | 19 | const document = _document as Document |
| 20 | +const window = _window as Window & typeof globalThis |
| 21 | + |
| 22 | +// Tour condition checking utilities (moved from utils/product-tour-utils.ts) |
| 23 | +function doesTourUrlMatch(tour: ProductTour): boolean { |
| 24 | + const conditions = tour.conditions |
| 25 | + if (!conditions?.url) { |
| 26 | + return true |
| 27 | + } |
| 28 | + |
| 29 | + const currentUrl = window.location.href |
| 30 | + const targetUrl = conditions.url |
| 31 | + const matchType = conditions.urlMatchType || 'contains' |
| 32 | + |
| 33 | + switch (matchType) { |
| 34 | + case 'exact': |
| 35 | + return currentUrl === targetUrl |
| 36 | + case 'contains': |
| 37 | + return currentUrl.includes(targetUrl) |
| 38 | + case 'regex': |
| 39 | + try { |
| 40 | + const regex = new RegExp(targetUrl) |
| 41 | + return regex.test(currentUrl) |
| 42 | + } catch { |
| 43 | + return false |
| 44 | + } |
| 45 | + default: |
| 46 | + return false |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +function doesTourSelectorMatch(tour: ProductTour): boolean { |
| 51 | + const conditions = tour.conditions |
| 52 | + if (!conditions?.selector) { |
| 53 | + return true |
| 54 | + } |
| 55 | + |
| 56 | + try { |
| 57 | + return !isNull(document.querySelector(conditions.selector)) |
| 58 | + } catch { |
| 59 | + return false |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +function isTourInDateRange(tour: ProductTour): boolean { |
| 64 | + const now = new Date() |
| 65 | + |
| 66 | + if (tour.start_date) { |
| 67 | + const startDate = new Date(tour.start_date) |
| 68 | + if (now < startDate) { |
| 69 | + return false |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + if (tour.end_date) { |
| 74 | + const endDate = new Date(tour.end_date) |
| 75 | + if (now > endDate) { |
| 76 | + return false |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + return true |
| 81 | +} |
| 82 | + |
| 83 | +function checkTourConditions(tour: ProductTour): boolean { |
| 84 | + return isTourInDateRange(tour) && doesTourUrlMatch(tour) && doesTourSelectorMatch(tour) |
| 85 | +} |
15 | 86 |
|
16 | 87 | const CONTAINER_CLASS = 'ph-product-tour-container' |
17 | 88 | const TRIGGER_LISTENER_ATTRIBUTE = 'data-ph-tour-trigger' |
@@ -119,16 +190,19 @@ export class ProductTourManager { |
119 | 190 | const activeTriggerTourIds = new Set<string>() |
120 | 191 |
|
121 | 192 | for (const tour of tours) { |
122 | | - // Tours with trigger_selector: always attach listener, skip eligibility |
| 193 | + // Determine the trigger selector - explicit trigger_selector takes precedence, |
| 194 | + // otherwise use conditions.selector for click-only tours (auto_launch=false) |
| 195 | + const triggerSelector = tour.trigger_selector || (!tour.auto_launch ? tour.conditions?.selector : null) |
| 196 | + |
| 197 | + // Tours with a trigger selector: always attach listener |
123 | 198 | // These are "on-demand" tours that show when clicked |
124 | | - if (tour.trigger_selector) { |
| 199 | + if (triggerSelector) { |
125 | 200 | activeTriggerTourIds.add(tour.id) |
126 | | - this._manageTriggerSelectorListener(tour) |
127 | | - continue |
| 201 | + this._manageTriggerSelectorListener({ ...tour, trigger_selector: triggerSelector }) |
128 | 202 | } |
129 | 203 |
|
130 | | - // Tours without trigger_selector: check eligibility for auto-show |
131 | | - if (!this._activeTour && this._isTourEligible(tour)) { |
| 204 | + // Only auto-show if auto_launch is enabled |
| 205 | + if (tour.auto_launch && !this._activeTour && this._isTourEligible(tour)) { |
132 | 206 | this.showTour(tour) |
133 | 207 | } |
134 | 208 | } |
@@ -391,7 +465,7 @@ export class ProductTourManager { |
391 | 465 | this._renderTooltipWithPreact(element) |
392 | 466 | } |
393 | 467 |
|
394 | | - private _renderTooltipWithPreact(element: HTMLElement): void { |
| 468 | + private _renderTooltipWithPreact(element: HTMLElement | null): void { |
395 | 469 | if (!this._activeTour) { |
396 | 470 | return |
397 | 471 | } |
@@ -487,4 +561,37 @@ export class ProductTourManager { |
487 | 561 | private _captureEvent(eventName: string, properties: Record<string, any>): void { |
488 | 562 | this._instance.capture(eventName, properties) |
489 | 563 | } |
| 564 | + |
| 565 | + // Public API methods delegated from PostHogProductTours |
| 566 | + getActiveProductTours(callback: ProductTourCallback): void { |
| 567 | + this._instance.productTours?.getProductTours((tours, context) => { |
| 568 | + if (!context?.isLoaded) { |
| 569 | + callback([], context) |
| 570 | + return |
| 571 | + } |
| 572 | + |
| 573 | + const activeTours = tours.filter((tour) => this._isTourEligible(tour)) |
| 574 | + callback(activeTours, context) |
| 575 | + }) |
| 576 | + } |
| 577 | + |
| 578 | + resetTour(tourId: string): void { |
| 579 | + localStore._remove(`ph_product_tour_completed_${tourId}`) |
| 580 | + localStore._remove(`ph_product_tour_dismissed_${tourId}`) |
| 581 | + } |
| 582 | + |
| 583 | + resetAllTours(): void { |
| 584 | + const storage = window?.localStorage |
| 585 | + if (!storage) { |
| 586 | + return |
| 587 | + } |
| 588 | + const keysToRemove: string[] = [] |
| 589 | + for (let i = 0; i < storage.length; i++) { |
| 590 | + const key = storage.key(i) |
| 591 | + if (key?.startsWith('ph_product_tour_completed_') || key?.startsWith('ph_product_tour_dismissed_')) { |
| 592 | + keysToRemove.push(key) |
| 593 | + } |
| 594 | + } |
| 595 | + keysToRemove.forEach((key) => localStore._remove(key)) |
| 596 | + } |
490 | 597 | } |
0 commit comments