diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..45cf2c1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + root: true, + extends: ['universe/native', 'universe/web'], + ignorePatterns: ['build'], +}; diff --git a/.gitignore b/.gitignore index 231a395..996c503 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ node_modules/ npm-debug.log yarn-error.log lib/ +# VSCode +.vscode/ +jsconfig.json # Xcode # @@ -41,3 +44,32 @@ local.properties buck-out/ \.buckd/ *.keystore +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml +android/app/libs +android/keystores/debug.keystore + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# Expo +.expo/* diff --git a/.npmignore b/.npmignore index 0cab2b3..937d158 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,14 @@ -build/ -android/build/ -android/BUCK -android/src/main/res/ -*.iml +# Exclude all top-level hidden directories by convention +/.*/ -yarn.lock -node_modules/ +# Exclude tarballs generated by `npm pack` +/*.tgz -.idea \ No newline at end of file +__mocks__ +__tests__ + +/babel.config.js +/android/src/androidTest/ +/android/src/test/ +/android/build/ +/example/ diff --git a/android/build.gradle b/android/build.gradle index ca78fc3..2a812f0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,135 +1,47 @@ -// android/build.gradle - -// based on: -// -// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle -// original location: -// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle -// -// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle -// original location: -// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle - -def DEFAULT_MIN_SDK_VERSION = 21 -def DEFAULT_TARGET_SDK_VERSION = 35 -def DEFAULT_COMPILE_SDK_VERSION = 35 -def DEFAULT_BUILD_TOOLS_VERSION = '35.0.0' - -def safeExtGet(prop, fallback) { - rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback -} - apply plugin: 'com.android.library' -apply plugin: 'maven-publish' - -buildscript { - // The Android Gradle plugin is only required when opening the android folder stand-alone. - // This avoids unnecessary downloads and potential conflicts when the library is included as a - // module dependency in an application project. - // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies - if (project == rootProject) { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' - } - } -} -android { - compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) - buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) +group = 'expo.modules.helpscout' +version = '0.1.0' + +def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") +apply from: expoModulesCorePlugin +applyKotlinExpoModulesCorePlugin() +useCoreDependencies() +useExpoPublishing() + +// If you want to use the managed Android SDK versions from expo-modules-core, set this to true. +// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code. +// Most of the time, you may like to manage the Android SDK versions yourself. +def useManagedAndroidSdkVersions = false +if (useManagedAndroidSdkVersions) { + useDefaultAndroidSdkVersions() +} else { + buildscript { + // Simple helper that allows the root project to override versions declared by this library. + ext.safeExtGet = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + } + project.android { + compileSdkVersion safeExtGet("compileSdkVersion", 36) defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) - targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) - versionCode 1 - versionName "1.0" - } - lintOptions { - abortOnError false - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } -} - -repositories { - mavenCentral() - // ref: https://www.baeldung.com/maven-local-repository - mavenLocal() - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url "$rootDir/../node_modules/react-native/android" - } - maven { - // Android JSC is installed from npm - url "$rootDir/../node_modules/jsc-android/dist" + minSdkVersion safeExtGet("minSdkVersion", 24) + targetSdkVersion safeExtGet("targetSdkVersion", 36) } - google() + } } - dependencies { - //noinspection GradleDynamicVersion - implementation 'com.facebook.react:react-native:+' // From node_modules - implementation "com.helpscout:beacon:6.0.1" -} - -def configureReactNativePom(def pom) { - def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text) - - pom.project { - name packageJson.title - artifactId packageJson.name - version = packageJson.version - group = "com.driversnote" - description packageJson.description - url packageJson.repository.baseUrl - - licenses { - license { - name packageJson.license - url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename - distribution 'repo' - } - } - - developers { - developer { - id packageJson.author.username - name packageJson.author.name - } - } - } + implementation "com.helpscout:beacon:6.0.1" } -afterEvaluate { project -> - task androidSourcesJar(type: Jar) { - archiveClassifier = 'sources' - from android.sourceSets.main.java.srcDirs - include '**/*.java' - } - android.libraryVariants.all { variant -> - def name = variant.name.capitalize() - def javaCompileTask = variant.javaCompileProvider.get() - - task "jar${name}"(type: Jar, dependsOn: javaCompileTask) { - from javaCompileTask.destinationDir - } - } - - artifacts { - archives androidSourcesJar - } - - publishing { - publications { - maven(MavenPublication) { - artifact androidSourcesJar - } - } - } +android { + namespace "expo.modules.helpscout" + defaultConfig { + versionCode 1 + versionName "0.1.0" + } + lintOptions { + abortOnError false + } } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index ba1b27b..bdae66c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,2 @@ - - + diff --git a/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutModule.kt b/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutModule.kt new file mode 100644 index 0000000..2ee068a --- /dev/null +++ b/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutModule.kt @@ -0,0 +1,139 @@ +package expo.modules.helpscout + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition +import java.net.URL +import android.text.TextUtils +import android.util.Log +import android.content.Context +import com.helpscout.beacon.Beacon +import com.helpscout.beacon.ui.BeaconActivity +import com.helpscout.beacon.model.BeaconScreens +import com.helpscout.beacon.model.SuggestedArticle + + +class ExpoHelpscoutModule : Module() { + private val TAG = "com.driversnote.helpscoutbeacon" + private var beacon: Beacon? = null + + // Each module class must implement the definition function. The definition consists of components + // that describes the module's functionality and behavior. + // See https://docs.expo.dev/modules/module-api for more details about available components. + override fun definition() = ModuleDefinition { + // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. + // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. + // The module will be accessible from `requireNativeModule('ExpoHelpscout')` in JavaScript. + Name("ExpoHelpscout") + + Function("init") { beaconID: String? -> + if (beaconID.isNullOrEmpty()) { + Log.w(TAG, "[init] Missing argument: beaconID") + return@Function + } + beacon = Beacon.Builder() + .withBeaconId(beaconID) + .build() + } + + Function("identify") { email: String?, name: String? -> + if (beacon == null || email.isNullOrEmpty() || name.isNullOrEmpty()) { + if (beacon == null) Log.w(TAG, "[identifyWithEmailAndName] Not initialized - did you forget to call 'init'?") + if (email.isNullOrEmpty()) Log.w(TAG, "[identifyWithEmailAndName] Missing argument: email") + if (name.isNullOrEmpty()) Log.w(TAG, "[identifyWithEmailAndName] Missing argument: name") + return@Function + } + Beacon.identify(email, name) + } + + Function("logout") { + if (beacon == null) { + Log.w(TAG, "[logout] Not initialized - did you forget to call 'init'?") + return@Function null + } + Beacon.logout() + null + } + + Function("addAttributeWithKey") { key: String?, value: String? -> + if (beacon == null || key.isNullOrEmpty() || value.isNullOrEmpty()) { + if (beacon == null) Log.w(TAG, "[addAttributeWithKey] Not initialized - did you forget to call 'init'?") + if (key.isNullOrEmpty()) Log.w(TAG, "[addAttributeWithKey] Missing argument: key") + if (value.isNullOrEmpty()) Log.w(TAG, "[addAttributeWithKey] Missing argument: value") + return@Function + } + Beacon.addAttributeWithKey(key, value) + } + + Function("open") { signature: String? -> + if (beacon == null) { + Log.w(TAG, "[open] Not initialized - did you forget to call 'init'?") + return@Function + } + val context = appContext.reactContext ?: return@Function + if (signature.isNullOrEmpty()) { + BeaconActivity.open(context) + } else { + BeaconActivity.openInSecureMode(context, signature) + } + } +// + Function("openArticle") { articleID: String?, signature: String? -> + if (beacon == null || articleID.isNullOrEmpty()) { + if (beacon == null) Log.w(TAG, "[openArticle] Not initialized - did you forget to call 'init'?") + if (articleID.isNullOrEmpty()) Log.w(TAG, "[openArticle] Missing argument: articleID") + return@Function + } + + val context = appContext.reactContext ?: return@Function + val articleList = arrayListOf(articleID) + + if (signature.isNullOrEmpty()) { + BeaconActivity.open(context, BeaconScreens.ARTICLE_SCREEN, articleList) + } else { + BeaconActivity.openInSecureMode(context, signature, BeaconScreens.ARTICLE_SCREEN, articleList) + } + } + + Function("navigate") { path: String? -> + if (beacon == null) { + Log.w(TAG, "[navigate] Not initialized - did you forget to call 'init'?") + return@Function + } + + val context = appContext.reactContext ?: return@Function + when (path) { + "/ask/message/" -> BeaconActivity.open(context, BeaconScreens.CONTACT_FORM_SCREEN, arrayListOf()) + "/ask/chat/" -> BeaconActivity.open(context, BeaconScreens.CHAT, arrayListOf()) + "/answers/" -> BeaconActivity.open(context, BeaconScreens.PREVIOUS_MESSAGES, arrayListOf()) + else -> Log.w(TAG, "[navigate] Path '${path ?: "null"}' not supported") + } + } + + Function("suggestArticles") { articleIds: List? -> + if (beacon == null) { + Log.w(TAG, "[suggestArticles] Not initialized - did you forget to call 'init'?") + return@Function + } + + if (articleIds.isNullOrEmpty()) { + Log.w(TAG, "[suggestArticles] Missing or empty articleIds") + return@Function + } + + val suggestedArticles = articleIds.take(5).map { + SuggestedArticle.SuggestedArticleWithId(it) + } + + Beacon.setOverrideSuggestedArticlesOrLinks(suggestedArticles) + } + + Function("resetSuggestions") { + if (beacon == null) { + Log.w(TAG, "[resetSuggestions] Not initialized - did you forget to call 'init'?") + return@Function null + } + Beacon.setOverrideSuggestedArticlesOrLinks(emptyList()) + } + + } +} diff --git a/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutView.kt b/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutView.kt new file mode 100644 index 0000000..157cfab --- /dev/null +++ b/android/src/main/java/expo/modules/helpscout/ExpoHelpscoutView.kt @@ -0,0 +1,30 @@ +package expo.modules.helpscout + +import android.content.Context +import android.webkit.WebView +import android.webkit.WebViewClient +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class ExpoHelpscoutView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + // Creates and initializes an event dispatcher for the `onLoad` event. + // The name of the event is inferred from the value and needs to match the event name defined in the module. + private val onLoad by EventDispatcher() + + // Defines a WebView that will be used as the root subview. + internal val webView = WebView(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + // Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript. + onLoad(mapOf("url" to url)) + } + } + } + + init { + // Adds the WebView to the view hierarchy. + addView(webView) + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 0000000..4aea052 --- /dev/null +++ b/example/App.tsx @@ -0,0 +1,42 @@ +import { useEvent } from 'expo'; +import ExpoHelpscout from 'expo-helpscout'; +import { useEffect } from 'react'; +import { Button, SafeAreaView, ScrollView, Text, View } from 'react-native'; + +const beaconId = ""; +const userEmail = ""; +const userName = ""; + +export default function App() { + + useEffect(() => { + ExpoHelpscout.init(beaconId); + ExpoHelpscout.identify(userEmail, userName); + }) + + return ( + + + Helpscout Example +