diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 834d3c7..c376537 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '20' - - run: npm ci --ignore-scripts + - run: npm install --ignore-scripts - run: npm run build - run: npm run lint - run: npm test diff --git a/.github/workflows/publish-svelte.yml b/.github/workflows/publish-svelte.yml new file mode 100644 index 0000000..4034898 --- /dev/null +++ b/.github/workflows/publish-svelte.yml @@ -0,0 +1,23 @@ +name: Publish @screeb/sdk-svelte + +on: + push: + tags: + - '@screeb/sdk-svelte@*' + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + - run: npm install -g npm@latest + - run: npm install --ignore-scripts + - run: npm run build + - run: npm publish --workspace=packages/sdk-svelte --access public --provenance diff --git a/examples/example-angular/package.json b/examples/example-angular/package.json index ae44394..5b2f61e 100644 --- a/examples/example-angular/package.json +++ b/examples/example-angular/package.json @@ -19,7 +19,7 @@ "scripts": { "build": "tsc && vite build", "clean": "rm -Rf dist", - "lint": "eslint .", + "lint": "eslint \"src/**/*.ts\"", "start": "vite" }, "dependencies": { @@ -37,10 +37,8 @@ "@typescript-eslint/eslint-plugin": "^6.7.5", "eslint": "^8.51.0", "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jest": "^27.4.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^5.0.1", - "jest": "^29.7.0", "typescript": "^5.2.2", "vite": "^4.4.11" } diff --git a/examples/example-browser/package.json b/examples/example-browser/package.json index 325bc27..4955afe 100644 --- a/examples/example-browser/package.json +++ b/examples/example-browser/package.json @@ -19,7 +19,7 @@ "scripts": { "build": "vite build", "clean": "rm -Rf dist", - "lint": "eslint .", + "lint": "eslint \"src/**/*.{ts,js}\"", "start": "vite" }, "dependencies": { @@ -32,11 +32,9 @@ "@typescript-eslint/eslint-plugin": "^6.7.5", "eslint": "^8.51.0", "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jest": "^27.4.2", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-svelte": "^2.34.0", - "jest": "^29.7.0", "svelte": "^4.2.1", "svelte-check": "^3.5.2", "tslib": "^2.6.2", diff --git a/examples/example-expo/README.md b/examples/example-expo/README.md index e06d562..f263aa8 100644 --- a/examples/example-expo/README.md +++ b/examples/example-expo/README.md @@ -1,55 +1,41 @@ -# Welcome to your Expo app 👋 +# `@screeb/react-native` Expo example -This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). +Minimal Expo app used to validate the Screeb React Native SDK with a generated native project. -> Screeb SDK documentation: [developers.screeb.app/sdk-react-native/install](https://developers.screeb.app/sdk-react-native/install) +> Documentation: [developers.screeb.app/sdk-react-native/install](https://developers.screeb.app/sdk-react-native/install) -## Get started +## Run -1. Install dependencies from the repository root (all workspaces share the same lockfile). +Install dependencies from the repository root: - ```bash - npm install - ``` - -2. Recreate the native project (new architecture is enabled by default and Screeb is autolinked via `react-native.config.js`). - - ```bash - npm run prebuild --workspace=example-expo - npm run ios --workspace=example-expo # or npm run android --workspace=example-expo - ``` - -3. Start the Metro server. - - ```bash - npm run example:expo --workspace=@screeb/react-native - ``` - -The generated build installs a fully native binary; Expo Go cannot load the Screeb TurboModule. Always launch the app produced by `expo run:ios` / `expo run:android` after the `prebuild` step. -If native sources fall out of sync, re-run `npm run prebuild --workspace=example-expo -- --clean` to regenerate the iOS and Android folders from scratch. - -You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). - -## Get a fresh project +```bash +npm install +``` -When you're ready, run: +Recreate the native project: ```bash -npm run reset-project --workspace=example-expo +npm run prebuild --workspace=example-expo ``` -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. +Run the native app: -## Learn more +```bash +npm run ios --workspace=example-expo +# or +npm run android --workspace=example-expo +``` -To learn more about developing your project with Expo, look at the following resources: +Start Metro from the React Native SDK workspace: -- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). -- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. +```bash +npm run example:expo --workspace=@screeb/react-native +``` -## Join the community +Expo Go cannot load the Screeb TurboModule. Use the app produced by `expo run:ios` or `expo run:android`. -Join our community of developers creating universal apps. +If native sources fall out of sync, regenerate them with: -- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. -- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. +```bash +npm run prebuild --workspace=example-expo -- --clean +``` diff --git a/examples/example-expo/app.json b/examples/example-expo/app.json index 83ffbac..06686df 100644 --- a/examples/example-expo/app.json +++ b/examples/example-expo/app.json @@ -4,47 +4,23 @@ "slug": "example-expo", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/icon.png", "scheme": "exampleexpo", - "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { "supportsTablet": true, "bundleIdentifier": "com.anonymous.example-expo" }, "android": { - "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/images/android-icon-foreground.png", - "backgroundImage": "./assets/images/android-icon-background.png", - "monochromeImage": "./assets/images/android-icon-monochrome.png" - }, "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false, "package": "com.anonymous.exampleexpo" }, - "web": { - "output": "static", - "favicon": "./assets/images/favicon.png" - }, "plugins": [ - "expo-router", - [ - "expo-splash-screen", - { - "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff", - "dark": { - "backgroundColor": "#000000" - } - } - ] + "./plugins/withScreebLocalSdk", + "expo-router" ], "experiments": { - "typedRoutes": true, - "reactCompiler": true + "typedRoutes": true } } } diff --git a/examples/example-expo/app/_layout.tsx b/examples/example-expo/app/_layout.tsx index f518c9b..a9aa203 100644 --- a/examples/example-expo/app/_layout.tsx +++ b/examples/example-expo/app/_layout.tsx @@ -1,24 +1,11 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; - -import { useColorScheme } from '@/hooks/use-color-scheme'; - -export const unstable_settings = { - anchor: '(tabs)', -}; +import { Stack } from "expo-router"; +import { StatusBar } from "expo-status-bar"; export default function RootLayout() { - const colorScheme = useColorScheme(); - return ( - - - - - + <> + - + ); } diff --git a/examples/example-expo/app/index.tsx b/examples/example-expo/app/index.tsx new file mode 100644 index 0000000..b190994 --- /dev/null +++ b/examples/example-expo/app/index.tsx @@ -0,0 +1,150 @@ +import * as Screeb from "@screeb/react-native"; +import { useEffect } from "react"; +import { + Alert, + Button, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; + +const PROJECT_TOKEN = "0e2b609a-8dce-4695-a80f-966fbfa87a88"; +const RESPONDENT_ID = "0021de43-6e44-443c-9903-2ab99f9c4233"; +const SURVEY_ID = "8dd42ae1-f716-429c-9843-fad62adf2ac4"; +const USER_PROPERTIES = { + locale: "fr-FR", + premium: true, +}; + +const initScreeb = async () => { + try { + await Screeb.initSdk( + PROJECT_TOKEN, + RESPONDENT_ID, + USER_PROPERTIES, + { + onReady: (payload: unknown) => { + console.log("Screeb ready", payload); + }, + onSurveyDisplayAllowed: () => { + console.log("Survey display allowed"); + return true; + }, + version: "1.0.0", + }, + {}, + ); + } catch (error) { + console.error("Failed to init Screeb", error); + Alert.alert("Screeb init failed", "Check the Metro logs for details."); + } +}; + +export default function ScreebExpoExample() { + useEffect(() => { + initScreeb(); + }, []); + + return ( + + @screeb/react-native + Expo + + Minimal Expo development build for exercising the Screeb React Native + wrapper. + + + + +``` + +## Identify after login + +```svelte + + +``` + +For a working example, see our [Screeb Svelte SDK example app](https://github.com/ScreebApp/sdk/tree/master/examples/example-svelte). + +## Documentation + +- Install guide: [developers.screeb.app/sdk-svelte/install](https://developers.screeb.app/sdk-svelte/install) +- API reference: [developers.screeb.app/sdk-svelte/reference](https://developers.screeb.app/sdk-svelte/reference) + +## Support + +For any issues, please contact our support team at support@screeb.com. + +## Contributing + +All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. + +## License + +Released under [MIT License](https://github.com/ScreebApp/sdk/blob/master/LICENSE). diff --git a/packages/sdk-svelte/package.json b/packages/sdk-svelte/package.json new file mode 100644 index 0000000..a639c49 --- /dev/null +++ b/packages/sdk-svelte/package.json @@ -0,0 +1,64 @@ +{ + "name": "@screeb/sdk-svelte", + "version": "0.1.0", + "description": "Screeb's browser SDK, optimized for Svelte.", + "keywords": [ + "product discovery", + "product management", + "survey", + "analytics", + "user feedback", + "user research", + "svelte" + ], + "homepage": "https://screeb.app", + "bugs": { + "url": "https://github.com/ScreebApp/sdk/issues", + "email": "support@screeb.app" + }, + "repository": { + "type": "git", + "url": "https://github.com/ScreebApp/sdk.git", + "directory": "packages/sdk-svelte" + }, + "license": "MIT", + "author": "Screeb's frontend team", + "sideEffects": false, + "type": "module", + "module": "dist/es/index.mjs", + "main": "dist/cjs/index.cjs", + "types": "dist/es/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "rollup -c ../../node_modules/@screeb/typescript-config/src/rollup.config.js", + "clean": "rm -Rf dist", + "lint": "eslint .", + "release:check": "npm run build --workspace=@screeb/sdk-browser && npm run build && npm run lint" + }, + "dependencies": { + "@screeb/sdk-browser": "^0.6.0" + }, + "devDependencies": { + "@screeb/eslint-config": "^0.1.6", + "@screeb/typescript-config": "^0.1.10", + "@types/node": "^20.8.4", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "eslint": "^8.51.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.0.3", + "rollup": "^4.0.2", + "svelte": ">=4.0.0", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "svelte": ">=4.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/sdk-svelte/readme/screeb-logo.svg b/packages/sdk-svelte/readme/screeb-logo.svg new file mode 100644 index 0000000..b74bd6e --- /dev/null +++ b/packages/sdk-svelte/readme/screeb-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/sdk-svelte/src/client.ts b/packages/sdk-svelte/src/client.ts new file mode 100644 index 0000000..5002b51 --- /dev/null +++ b/packages/sdk-svelte/src/client.ts @@ -0,0 +1,166 @@ +import * as Screeb from "@screeb/sdk-browser"; + +import CONSTANTS from "./constants"; +import * as logger from "./logger"; +import { ScreebClient, ScreebConfig } from "./types"; + +export const createScreebClient = (config: ScreebConfig): ScreebClient => { + let isInitialized = false; + + const ensureScreeb = async function ReturnType>( + functionName: string, + callback: T, + onlyLoaded = false, + ): Promise> { + const shouldLoad = config.shouldLoad ?? true; + if (!Screeb.isLoaded() && !shouldLoad) { + const message = + "Screeb instance is not loaded because `shouldLoad` is set to `false` in the Svelte provider"; + logger.log("warn", message); + return Promise.reject(message); + } + + if (!isInitialized && !onlyLoaded) { + const message = [ + `"${functionName}" was called but Screeb has not been initialized yet. `, + `Please call 'init' before calling '${functionName}' or `, + "set 'autoInit' to true in the Svelte provider config.", + ].join(""); + logger.log("warn", message); + return Promise.reject(message); + } + + return Promise.resolve(callback()); + }; + + const init = async ( + websiteId: string, + userId?: string, + userProperties?: Screeb.PropertyRecord, + hooks?: Screeb.HooksInit, + language?: string, + ) => { + await ensureScreeb( + "init", + () => { + if (!isInitialized) { + Screeb.init(websiteId, userId, userProperties, hooks, language); + isInitialized = true; + } + }, + true, + ); + }; + + const load = async (options?: Screeb.ScreebOptions) => { + if (!Screeb.isLoaded()) { + Screeb.load({ + sdkName: "sdk-svelte", + sdkVersion: CONSTANTS.version, + ...options, + }); + } + + if (config.autoInit && !isInitialized) { + if (config.websiteId) { + await init( + config.websiteId, + config.userId, + config.userProperties, + config.hooks, + config.language, + ); + } else { + logger.log( + "warn", + "autoInit is set to true, but no websiteId have been provided.", + ); + } + } + }; + + const close = async () => { + if (Screeb.isLoaded()) { + await Screeb.close(); + isInitialized = false; + } + }; + + return { + ScreebId: Screeb.ScreebId, + ScreebMaskText: Screeb.ScreebMaskText, + ScreebNoCapture: Screeb.ScreebNoCapture, + close, + debug: async () => ensureScreeb("debug", () => Screeb.debug()), + eventTrack: async (eventName, eventProperties) => + ensureScreeb("eventTrack", () => + Screeb.eventTrack(eventName, eventProperties), + ), + identity: async (userId, userProperties) => + ensureScreeb("identity", () => Screeb.identity(userId, userProperties)), + identityGet: async () => + ensureScreeb("identityGet", () => Screeb.identityGet()), + identityGroupAssign: async (groupName, groupType, groupProperties) => + ensureScreeb("identityGroupAssign", () => + Screeb.identityGroupAssign(groupName, groupType, groupProperties), + ), + identityGroupUnassign: async (groupName, groupType) => + ensureScreeb("identityGroupUnassign", () => + Screeb.identityGroupUnassign(groupName, groupType), + ), + identityProperties: async (userProperties) => + ensureScreeb("identityProperties", () => + Screeb.identityProperties(userProperties), + ), + identityReset: async () => + ensureScreeb("identityReset", () => Screeb.identityReset()), + init, + load, + messageClose: async () => + ensureScreeb("messageClose", () => Screeb.messageClose()), + messageStart: async ( + messageId, + allowMultipleResponses, + hiddenFields, + hooks, + language, + ) => + ensureScreeb("messageStart", () => + Screeb.messageStart( + messageId, + allowMultipleResponses, + hiddenFields, + hooks, + language, + ), + ), + sessionReplayStart: async () => + ensureScreeb("sessionReplayStart", () => Screeb.sessionReplayStart()), + sessionReplayStop: async () => + ensureScreeb("sessionReplayStop", () => Screeb.sessionReplayStop()), + surveyClose: async () => + ensureScreeb("surveyClose", () => Screeb.surveyClose()), + surveyStart: async ( + surveyId, + distributionId, + allowMultipleResponses, + hiddenFields, + hooks, + language, + selectors, + ) => + ensureScreeb("surveyStart", () => + Screeb.surveyStart( + surveyId, + distributionId, + allowMultipleResponses, + hiddenFields, + hooks, + language, + selectors, + ), + ), + targetingDebug: async () => + ensureScreeb("targetingDebug", () => Screeb.targetingDebug()), + }; +}; diff --git a/packages/sdk-svelte/src/constants.ts b/packages/sdk-svelte/src/constants.ts new file mode 100644 index 0000000..fe9cb36 --- /dev/null +++ b/packages/sdk-svelte/src/constants.ts @@ -0,0 +1,3 @@ +const CONSTANTS = { version: "0.1.0" }; + +export default CONSTANTS; diff --git a/packages/sdk-svelte/src/context.ts b/packages/sdk-svelte/src/context.ts new file mode 100644 index 0000000..a538615 --- /dev/null +++ b/packages/sdk-svelte/src/context.ts @@ -0,0 +1,35 @@ +import { getContext, setContext } from "svelte"; + +import { createScreebClient } from "./client"; +import * as logger from "./logger"; +import { ScreebClient, ScreebConfig } from "./types"; +import { isSSR } from "./utils"; + +export const SCREEB_CONTEXT_KEY = Symbol("screeb"); + +export function setScreebContext(config: ScreebConfig): ScreebClient { + const client = createScreebClient(config); + + setContext(SCREEB_CONTEXT_KEY, client); + + if (!isSSR && (config.shouldLoad ?? true)) { + void client.load(config.options); + } + + return client; +} + +export const provideScreeb = setScreebContext; + +export function useScreeb(): ScreebClient { + const context = getContext(SCREEB_CONTEXT_KEY); + + if (!context) { + logger.log( + "warn", + "`useScreeb` must be called inside a component tree where `setScreebContext` or `provideScreeb` has been called.", + ); + } + + return context as ScreebClient; +} diff --git a/packages/sdk-svelte/src/index.ts b/packages/sdk-svelte/src/index.ts new file mode 100644 index 0000000..eb6b921 --- /dev/null +++ b/packages/sdk-svelte/src/index.ts @@ -0,0 +1,8 @@ +export { createScreebClient } from "./client"; +export { + provideScreeb, + SCREEB_CONTEXT_KEY, + setScreebContext, + useScreeb, +} from "./context"; +export * from "./types"; diff --git a/packages/sdk-svelte/src/logger.ts b/packages/sdk-svelte/src/logger.ts new file mode 100644 index 0000000..698a24c --- /dev/null +++ b/packages/sdk-svelte/src/logger.ts @@ -0,0 +1,8 @@ +type LogLevel = "warn"; + +export const log = (level: LogLevel, message: string) => { + if (level === "warn") { + // eslint-disable-next-line no-console + console.warn(message); + } +}; diff --git a/packages/sdk-svelte/src/types.ts b/packages/sdk-svelte/src/types.ts new file mode 100644 index 0000000..d60547d --- /dev/null +++ b/packages/sdk-svelte/src/types.ts @@ -0,0 +1,132 @@ +/* eslint-disable no-unused-vars */ +import { + HooksInit, + HooksMessageStart, + HooksSurveyStart, + PropertyRecord, + ScreebIdentityGetReturn, + ScreebOptions, +} from "@screeb/sdk-browser"; + +/** Configuration for the Svelte Screeb provider. */ +export type ScreebConfig = { + /** Your website/channel id. */ + websiteId: string; + /** The unique identifier of your user. */ + userId?: string; + /** The properties of your user. */ + userProperties?: PropertyRecord; + /** Hooks to define callbacks for Screeb events. */ + hooks?: HooksInit; + /** Force a specific language (for example, "en"). Defaults to the browser language. */ + language?: string; + /** + * Indicates if Screeb should be automatically loaded. + * Set to false to prevent the SDK from loading, for example during SSR or tests. + * @default true + */ + shouldLoad?: boolean; + /** + * Indicates if Screeb should be automatically initialized after load. + * @default false + */ + autoInit?: boolean; + /** Screeb tag loading options. */ + options?: ScreebOptions; +}; + +export type CloseFunction = () => Promise; +export type DebugFunction = () => Promise; + +export type EventTrackFunction = ( + eventName: string, + eventProperties?: PropertyRecord, +) => Promise; + +export type IdentityFunction = ( + userId: string, + userProperties?: PropertyRecord, +) => Promise; + +export type IdentityGetFunction = () => Promise; + +export type IdentityGroupAssignFunction = ( + groupName: string, + groupType?: string, + groupProperties?: PropertyRecord, +) => Promise; + +export type IdentityGroupUnassignFunction = ( + groupName: string, + groupType?: string, +) => Promise; + +export type IdentityPropertiesFunction = ( + userProperties: PropertyRecord, +) => Promise; + +export type IdentityResetFunction = () => Promise; + +export type InitFunction = ( + websiteId: string, + userId?: string, + userProperties?: PropertyRecord, + hooks?: HooksInit, + language?: string, +) => Promise; + +export type LoadFunction = (options?: ScreebOptions) => Promise; + +export type SurveyCloseFunction = () => Promise; + +export type SurveyStartFunction = ( + surveyId: string, + distributionId?: string, + allowMultipleResponses?: boolean, + hiddenFields?: PropertyRecord, + hooks?: HooksSurveyStart, + language?: string, + selectors?: string | string[], +) => Promise; + +export type MessageCloseFunction = () => Promise; + +export type MessageStartFunction = ( + messageId: string, + allowMultipleResponses?: boolean, + hiddenFields?: PropertyRecord, + hooks?: HooksMessageStart, + language?: string, +) => Promise; + +export type SessionReplayStopFunction = () => Promise; +export type SessionReplayStartFunction = () => Promise; +export type TargetingDebugFunction = () => Promise; +export type ScreebMaskTextFunction = (element: T) => T; +export type ScreebNoCaptureFunction = (element: T) => T; +export type ScreebIdFunction = (element: T, id: string) => T; + +/** All Screeb methods provided by `useScreeb()`. */ +export type ScreebClient = { + close: CloseFunction; + debug: DebugFunction; + eventTrack: EventTrackFunction; + identity: IdentityFunction; + identityGet: IdentityGetFunction; + identityGroupAssign: IdentityGroupAssignFunction; + identityGroupUnassign: IdentityGroupUnassignFunction; + identityProperties: IdentityPropertiesFunction; + identityReset: IdentityResetFunction; + init: InitFunction; + load: LoadFunction; + messageClose: MessageCloseFunction; + messageStart: MessageStartFunction; + sessionReplayStart: SessionReplayStartFunction; + sessionReplayStop: SessionReplayStopFunction; + surveyClose: SurveyCloseFunction; + surveyStart: SurveyStartFunction; + targetingDebug: TargetingDebugFunction; + ScreebMaskText: ScreebMaskTextFunction; + ScreebNoCapture: ScreebNoCaptureFunction; + ScreebId: ScreebIdFunction; +}; diff --git a/packages/sdk-svelte/src/utils.ts b/packages/sdk-svelte/src/utils.ts new file mode 100644 index 0000000..19c0976 --- /dev/null +++ b/packages/sdk-svelte/src/utils.ts @@ -0,0 +1 @@ +export const isSSR = typeof window === "undefined"; diff --git a/packages/sdk-svelte/tsconfig.json b/packages/sdk-svelte/tsconfig.json new file mode 100644 index 0000000..5c272f8 --- /dev/null +++ b/packages/sdk-svelte/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@screeb/typescript-config/src/tsconfig.json", + "include": ["src"], + "exclude": ["dist", "**/*.test.ts"], + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationDir": "." + } +} diff --git a/packages/sdk-vue/docs/README.md b/packages/sdk-vue/docs/README.md index c60574e..90115a1 100644 --- a/packages/sdk-vue/docs/README.md +++ b/packages/sdk-vue/docs/README.md @@ -21,6 +21,9 @@ - [MessageStartFunction](README.md#messagestartfunction) - [ScreebConfig](README.md#screebconfig) - [ScreebContextValues](README.md#screebcontextvalues) +- [ScreebIdFunction](README.md#screebidfunction) +- [ScreebMaskTextFunction](README.md#screebmasktextfunction) +- [ScreebNoCaptureFunction](README.md#screebnocapturefunction) - [SessionReplayStartFunction](README.md#sessionreplaystartfunction) - [SessionReplayStopFunction](README.md#sessionreplaystopfunction) - [SurveyCloseFunction](README.md#surveyclosefunction) @@ -312,6 +315,9 @@ All Screeb methods provided via `useScreeb()` | Name | Type | | :------ | :------ | +| `ScreebId` | [`ScreebIdFunction`](README.md#screebidfunction) | +| `ScreebMaskText` | [`ScreebMaskTextFunction`](README.md#screebmasktextfunction) | +| `ScreebNoCapture` | [`ScreebNoCaptureFunction`](README.md#screebnocapturefunction) | | `close` | [`CloseFunction`](README.md#closefunction) | | `debug` | [`DebugFunction`](README.md#debugfunction) | | `eventTrack` | [`EventTrackFunction`](README.md#eventtrackfunction) | @@ -333,6 +339,85 @@ All Screeb methods provided via `useScreeb()` ___ +### ScreebIdFunction + +Ƭ **ScreebIdFunction**: \(`element`: `T`, `id`: `string`) => `T` + +#### Type declaration + +▸ \<`T`\>(`element`, `id`): `T` + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `Element` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `element` | `T` | +| `id` | `string` | + +##### Returns + +`T` + +___ + +### ScreebMaskTextFunction + +Ƭ **ScreebMaskTextFunction**: \(`element`: `T`) => `T` + +#### Type declaration + +▸ \<`T`\>(`element`): `T` + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `Element` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `element` | `T` | + +##### Returns + +`T` + +___ + +### ScreebNoCaptureFunction + +Ƭ **ScreebNoCaptureFunction**: \(`element`: `T`) => `T` + +#### Type declaration + +▸ \<`T`\>(`element`): `T` + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `Element` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `element` | `T` | + +##### Returns + +`T` + +___ + ### SessionReplayStartFunction Ƭ **SessionReplayStartFunction**: () => `Promise`\<`unknown`\> diff --git a/packages/sdk-vue/src/plugin.ts b/packages/sdk-vue/src/plugin.ts index 4f0eaed..2531c58 100644 --- a/packages/sdk-vue/src/plugin.ts +++ b/packages/sdk-vue/src/plugin.ts @@ -231,6 +231,9 @@ export const ScreebPlugin: Plugin = { ); const context: ScreebContextValues = { + ScreebId: Screeb.ScreebId, + ScreebMaskText: Screeb.ScreebMaskText, + ScreebNoCapture: Screeb.ScreebNoCapture, close, debug, eventTrack, @@ -242,12 +245,12 @@ export const ScreebPlugin: Plugin = { identityReset, init, load, - surveyClose, - surveyStart, messageClose, messageStart, - sessionReplayStop, sessionReplayStart, + sessionReplayStop, + surveyClose, + surveyStart, targetingDebug, }; diff --git a/packages/sdk-vue/src/types.ts b/packages/sdk-vue/src/types.ts index 9d5c3fa..ded1330 100644 --- a/packages/sdk-vue/src/types.ts +++ b/packages/sdk-vue/src/types.ts @@ -103,6 +103,9 @@ export type MessageStartFunction = ( export type SessionReplayStopFunction = () => Promise; export type SessionReplayStartFunction = () => Promise; export type TargetingDebugFunction = () => Promise; +export type ScreebMaskTextFunction = (element: T) => T; +export type ScreebNoCaptureFunction = (element: T) => T; +export type ScreebIdFunction = (element: T, id: string) => T; /** All Screeb methods provided via `useScreeb()` */ export type ScreebContextValues = { @@ -124,4 +127,7 @@ export type ScreebContextValues = { sessionReplayStart: SessionReplayStartFunction; sessionReplayStop: SessionReplayStopFunction; targetingDebug: TargetingDebugFunction; + ScreebMaskText: ScreebMaskTextFunction; + ScreebNoCapture: ScreebNoCaptureFunction; + ScreebId: ScreebIdFunction; }; diff --git a/scripts/sync-sdk-versions.mjs b/scripts/sync-sdk-versions.mjs new file mode 100755 index 0000000..3a08b65 --- /dev/null +++ b/scripts/sync-sdk-versions.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const manifestPath = resolve(root, "sdk-versions.json"); +const semverPattern = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; + +function readManifest() { + return JSON.parse(readFileSync(manifestPath, "utf8")); +} + +function writeJson(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); +} + +function setByPath(object, dottedPath, value) { + const parts = dottedPath.split("."); + let current = object; + for (const part of parts.slice(0, -1)) { + if (!current[part] || typeof current[part] !== "object") { + throw new Error(`Unknown version path: ${dottedPath}`); + } + current = current[part]; + } + const leaf = parts.at(-1); + if (!leaf || !(leaf in current)) { + throw new Error(`Unknown version path: ${dottedPath}`); + } + current[leaf] = value; +} + +function replaceInFile(file, pattern, replacement) { + const path = resolve(root, file); + const before = readFileSync(path, "utf8"); + if (!pattern.test(before)) { + throw new Error(`No version match found in ${file}`); + } + const after = before.replace(pattern, replacement); + writeIfNeeded(file, before, after); + if (!check && after !== before) { + console.log(`${file}`); + } +} + +function replacePackageVersion(file, version) { + const path = resolve(root, file); + const before = readFileSync(path, "utf8"); + const json = JSON.parse(before); + json.version = version; + const after = `${JSON.stringify(json, null, 2)}\n`; + writeIfNeeded(file, before, after); + if (!check && after !== before) { + console.log(`${file}`); + } +} + +function parseArgs(args) { + const sets = []; + let check = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--check") { + check = true; + continue; + } + if (arg === "--set") { + const path = args[index + 1]; + const version = args[index + 2]; + if (!path || !version) { + throw new Error("Usage: node scripts/sync-sdk-versions.mjs --set "); + } + sets.push([path, version]); + index += 2; + continue; + } + throw new Error(`Unknown argument: ${arg}`); + } + + return { check, sets }; +} + +const { check, sets } = parseArgs(process.argv.slice(2)); +const manifest = readManifest(); +const changedFiles = []; + +function writeIfNeeded(file, before, after) { + if (after === before) { + return; + } + if (check) { + changedFiles.push(file); + return; + } + writeFileSync(resolve(root, file), after); +} + +for (const [path, version] of sets) { + if (!semverPattern.test(version)) { + throw new Error(`Invalid semantic version for ${path}: ${version}`); + } + setByPath(manifest, path, version); +} + +if (sets.length > 0 && !check) { + writeJson(manifestPath, manifest); + console.log("sdk-versions.json"); +} + +const androidVersion = manifest.native.android; +const iosVersion = manifest.native.ios; +const flutterVersion = manifest.wrappers.flutter; +const kmpVersion = manifest.wrappers.kmp; +const mauiVersion = manifest.wrappers.maui; +const reactNativeVersion = manifest.wrappers.reactNative; +const svelteVersion = manifest.wrappers.svelte; + +replacePackageVersion("packages/sdk-reactnative/package.json", reactNativeVersion); +replacePackageVersion("packages/sdk-svelte/package.json", svelteVersion); +replaceInFile("packages/sdk-flutter/pubspec.yaml", /^version: .+$/m, `version: ${flutterVersion}`); +replaceInFile("packages/sdk-flutter/android/build.gradle", /^version '.+'$/m, `version '${flutterVersion}'`); +replaceInFile("packages/sdk-flutter/android/build.gradle", /api "app\.screeb\.sdk:survey:[^"]+"/, `api "app.screeb.sdk:survey:${androidVersion}"`); +replaceInFile("packages/sdk-flutter/ios/plugin_screeb.podspec", /s\.version\s+=\s+'[^']+'/, `s.version = '${flutterVersion}'`); +replaceInFile("packages/sdk-flutter/ios/plugin_screeb.podspec", /s\.dependency 'Screeb', '[^']+'/, `s.dependency 'Screeb', '${iosVersion}'`); +replaceInFile("packages/sdk-reactnative/android/build.gradle", /api "app\.screeb\.sdk:survey:[^"]+"/, `api "app.screeb.sdk:survey:${androidVersion}"`); +replaceInFile("packages/sdk-reactnative/ScreebReactNative.podspec", /s\.dependency "Screeb", '~> [^']+'/, `s.dependency "Screeb", '~> ${iosVersion}'`); +replaceInFile( + "packages/sdk-reactnative/android/src/main/java/app/screeb/reactnative/ScreebReactNativeModule.kt", + /Screeb\.setSecondarySDK\("react-native", "[^"]+"\)/, + `Screeb.setSecondarySDK("react-native", "${reactNativeVersion}")`, +); +replaceInFile( + "packages/sdk-reactnative/ios/ScreebReactNative.swift", + /Screeb\.setSecondarySDK\(name: "react-native", version: "[^"]+"\)/, + `Screeb.setSecondarySDK(name: "react-native", version: "${reactNativeVersion}")`, +); +replaceInFile( + "packages/sdk-flutter/android/src/main/kotlin/app/screeb/plugin_screeb/PluginScreebPlugin.kt", + /Screeb\.setSecondarySDK\("flutter", "[^"]+"\)/, + `Screeb.setSecondarySDK("flutter", "${flutterVersion}")`, +); +replaceInFile( + "packages/sdk-flutter/ios/Classes/SwiftPluginScreebPlugin.swift", + /Screeb\.setSecondarySDK\(name: "flutter", version: "[^"]+"\)/, + `Screeb.setSecondarySDK(name: "flutter", version: "${flutterVersion}")`, +); +replaceInFile("packages/sdk-kmp/gradle.properties", /^VERSION_NAME=.*$/m, `VERSION_NAME=${kmpVersion}`); +replaceInFile("packages/sdk-kmp/gradle.properties", /^SCREEB_ANDROID_SDK_VERSION=.*$/m, `SCREEB_ANDROID_SDK_VERSION=${androidVersion}`); +replaceInFile("packages/sdk-kmp/gradle.properties", /^SCREEB_IOS_SDK_VERSION=.*$/m, `SCREEB_IOS_SDK_VERSION=${iosVersion}`); +replaceInFile( + "packages/sdk-kmp/src/commonMain/kotlin/app/screeb/sdk/kmp/SdkVersion.kt", + /internal const val SDK_VERSION = "[^"]+"/, + `internal const val SDK_VERSION = "${kmpVersion}"`, +); +replaceInFile( + "packages/sdk-svelte/src/constants.ts", + /const CONSTANTS = \{ version: "[^"]+" \};/, + `const CONSTANTS = { version: "${svelteVersion}" };`, +); +replaceInFile("packages/sdk-maui/ScreebMaui.csproj", /()[^<]+(<\/Version>)/, `$1${mauiVersion}$2`); +replaceInFile( + "packages/sdk-maui/ScreebMaui.csproj", + /(Include="app\.screeb\.sdk:survey"\s*\n\s*Version=")[^"]+(")/, + `$1${androidVersion}$2`, +); +replaceInFile("examples/example-android/app/build.gradle", /implementation 'app\.screeb\.sdk:survey:[^']+'/, `implementation 'app.screeb.sdk:survey:${androidVersion}'`); + +if (check && changedFiles.length > 0) { + console.error(`Version files are not synced:\n${changedFiles.map((file) => `- ${file}`).join("\n")}`); + process.exit(1); +} + +if (check) { + console.log("Version files are synced."); +} diff --git a/sdk-versions.json b/sdk-versions.json new file mode 100644 index 0000000..98b1edb --- /dev/null +++ b/sdk-versions.json @@ -0,0 +1,13 @@ +{ + "native": { + "android": "4.0.0", + "ios": "4.0.0" + }, + "wrappers": { + "flutter": "3.1.0", + "kmp": "0.1.0", + "maui": "0.1.0", + "reactNative": "3.3.1", + "svelte": "0.1.0" + } +}