Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add new package specific to Pro features #2870

Merged
merged 6 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@astrojs/check": "^0.7.0",
"@astrojs/starlight": "^0.23.2",
"@astrojs/tailwind": "^5.1.0",
"astro": "^4.8.4",
"astro": "^4.8.6",
"sharp": "^0.33.3",
"shepherd.js": "workspace:*",
"starlight-typedoc": "^0.10.1",
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"autoprefixer": "^10.4.19",
"better-docs": "^2.7.3",
"concurrently": "^8.2.2",
"del": "^7.1.0",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.5.0",
"eslint-plugin-svelte": "^2.39.0",
"postcss": "^8.4.38",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.1.1",
"prettier-plugin-svelte": "^3.2.2",
"release-it": "^17.3.0",
Expand Down
13 changes: 13 additions & 0 deletions packages/pro-js/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @shepherdpro/pro-js

## 0.0.3

### Patch Changes

- Fix exports for published packages

## 0.0.2

### Patch Changes

- Improve types and reusing the Pro JS module
5 changes: 5 additions & 0 deletions packages/pro-js/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @shepherdpro/pro-js
chuckcarpenter marked this conversation as resolved.
Show resolved Hide resolved

Simple and beautiful user journeys.

Add onboarding, product hints, feature release notifications, and so much more...
41 changes: 41 additions & 0 deletions packages/pro-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@shepherdpro/pro-js",
"version": "0.0.3",
"private": false,
"main": "./dist/index.umd.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.umd.cjs"
},
"./package.json": "./package.json"
},
"type": "module",
"files": [
"dist",
"src"
],
"scripts": {
"build": "vite build",
"test:ci": "vitest --run",
"test:dev": "vitest"
},
"devDependencies": {
"@vitest/ui": "^1.6.0",
"fake-indexeddb": "^5.0.2",
"happy-dom": "^12.10.3",
"vite": "^5.2.11",
"vite-plugin-dts": "^3.7.1",
"vitest": "^1.6.0"
},
"dependencies": {
"shepherd.js": "workspace:*",
"idb": "^8.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ interface TourStateDb extends DBSchema {
}

class DataRequest {
apiKey: string;
apiPath: string;
properties?: { [key: string]: unknown };
private apiKey: string;
private apiPath: string;
private properties?: { [key: string]: unknown };
chuckcarpenter marked this conversation as resolved.
Show resolved Hide resolved
tourStateDb?: IDBPDatabase<TourStateDb>;

constructor(
Expand All @@ -34,6 +34,14 @@ class DataRequest {
this.properties = properties;
}

getConfig() {
return {
apiKey: this.apiKey,
apiPath: this.apiPath,
properties: this.properties
};
}

/**
* Gets a list of the state for all the tours associated with a given apiKey
*/
Expand Down
148 changes: 148 additions & 0 deletions packages/pro-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import Shepherd, { ShepherdBase } from 'shepherd.js';
import type { TourOptions, EventOptions } from 'shepherd.js/tour';

import DataRequest from './DataRequest';
import { getContext } from './utils/context.ts';

interface Actor {
actorId: number;
}

const SHEPHERD_DEFAULT_API = 'https://shepherdpro.com' as const;
const SHEPHERD_USER_ID = 'shepherdPro:userId' as const;

class ProTour extends Shepherd.Tour {
public events = ['active', 'cancel', 'complete', 'show'];

private currentUserId: string | null = null;

constructor(options?: TourOptions) {
super(options);

this.currentUserId = localStorage.getItem(SHEPHERD_USER_ID);

const { dataRequester } = ShepherdProInstance;

if (dataRequester) {
this.trackedEvents.forEach((event) =>
this.on(event, (opts: EventOptions) => {
const { tour } = opts;
const { id, steps } = tour;
let position;

if (event !== 'active') {
const { step: currentStep } = opts;

if (currentStep) {
position =
steps.findIndex((step) => step.id === currentStep.id) + 1;
}
}

const data = {
currentUserId: this.currentUserId,
eventType: event,
journeyData: {
id,
currentStep: position,
numberOfSteps: steps.length,
tourOptions: tour.options
}
};
dataRequester?.sendEvents({ data });
})
);
}
}
}

export class ShepherdPro extends ShepherdBase {
// Shepherd Pro fields
apiKey?: string;
apiPath?: string;
dataRequester?: DataRequest;
isProEnabled = false;
/**
* Extra properties to pass to Shepherd Pro App
*/
properties?: { [key: string]: unknown };

constructor() {
super();

this.Tour = ProTour;
}

/**
* Call init to take full advantage of ShepherdPro functionality
* @param {string} apiKey The API key for your ShepherdPro account
* @param {string} apiPath
* @param {object} properties Extra properties to be passed to Shepherd Pro
*/
async init(
apiKey?: string,
apiPath?: string,
properties?: { [key: string]: unknown }
) {
if (!apiKey) {
throw new Error('Shepherd Pro: Missing required apiKey option.');
}

this.apiKey = apiKey;
this.apiPath = apiPath ?? SHEPHERD_DEFAULT_API;
this.properties = properties ?? {};
this.properties['context'] = getContext(window);

if (this.apiKey) {
this.dataRequester = new DataRequest(
this.apiKey,
this.apiPath,
this.properties
);

// Setup actor before first tour is loaded if none exists
const shepherdProId = localStorage.getItem(SHEPHERD_USER_ID);

const promises = [this.dataRequester.getTourState()];

if (!shepherdProId) {
promises.push(this.createNewActor());
}

await Promise.all(promises);
}
}

async createNewActor() {
if (!this.dataRequester) return;

// Setup type returns an actor
const response = (await this.dataRequester.sendEvents({
data: {
currentUserId: null,
eventType: 'setup'
}
})) as unknown as Actor;

localStorage.setItem(SHEPHERD_USER_ID, String(response.actorId));
}

/**
* Checks if a given tour's id is enabled
* @param tourId A string denoting the id of the tour
*/
async isTourEnabled(tourId: string) {
if (!this.dataRequester) return;

const tourState = await this.dataRequester.tourStateDb?.get(
'tours',
tourId
);

return tourState?.isActive ?? true;
}
}

const ShepherdProInstance = new ShepherdPro();

export default Object.assign(ShepherdProInstance, Shepherd) as ShepherdPro;
File renamed without changes.
61 changes: 61 additions & 0 deletions packages/pro-js/test/datarequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type Mock, afterAll, describe, expect, it, vi } from 'vitest';

import DataRequest from '../src/DataRequest';

global.fetch = vi.fn();

describe('DataRequest', () => {
const dataRequesterMock = vi
.spyOn(DataRequest.prototype, 'sendEvents')
.mockImplementation(() => Promise.resolve({}));

afterAll(() => {
dataRequesterMock.mockReset();
});

it('exists and creates an instance', () => {
const requestInstance = new DataRequest(
'apiKey_12345',
'https://shepherdpro.com',
{ extra: 'stuff' }
);

expect(DataRequest).toBeTruthy();
expect(requestInstance).toBeInstanceOf(DataRequest);
});

it('returns an error if no apiKey is passed', () => {
expect(() => new DataRequest()).toThrow(
'Shepherd Pro: Missing required apiKey option.'
);
});

it('returns an error if no apiPath is passed', () => {
expect(() => new DataRequest('apiKey_12345')).toThrow(
'Shepherd Pro: Missing required apiPath option.'
);
});

it('can use the dataRequester to sendEvents()', async () => {
(fetch as Mock).mockResolvedValue({
ok: true,
json: () => new Promise((resolve) => resolve({ data: {} }))
});
const dataRequester = new DataRequest(
'apiKey_12345',
'https://shepherdpro.com',
{ extra: 'stuff' }
);

expect(typeof dataRequester.sendEvents).toBe('function');
expect(dataRequester.getConfig().apiKey).toBe('apiKey_12345');
expect(dataRequester.getConfig().apiPath).toBe('https://shepherdpro.com');
expect(dataRequester.getConfig().properties).toMatchObject({
extra: 'stuff'
});

const data = await dataRequester.sendEvents({ data: {} });

expect(data).toMatchObject({});
});
});
Loading