diff --git a/.gitignore b/.gitignore index 09a501ae..a0de5287 100644 --- a/.gitignore +++ b/.gitignore @@ -48,7 +48,7 @@ yarn-error.log* npm-debug.log yarn-error.log vendor/ -turbo-android-dependencies.gradle +hotwire-native-android-dependencies.gradle # macOS .DS_Store diff --git a/examples/turbo-demo-expo-example/app.json b/examples/turbo-demo-expo-example/app.json index f43df4b4..a9c3e70c 100644 --- a/examples/turbo-demo-expo-example/app.json +++ b/examples/turbo-demo-expo-example/app.json @@ -34,7 +34,8 @@ "expo-build-properties", { "android": { - "minSdkVersion": 26 + "minSdkVersion": 28, + "kotlinVersion": "1.9.25" }, "ios": { "deploymentTarget": "15.6" diff --git a/packages/turbo/README.md b/packages/turbo/README.md index 7c66817f..1164b4c6 100644 --- a/packages/turbo/README.md +++ b/packages/turbo/README.md @@ -25,9 +25,9 @@ For Android you need to adjust your SDK version in your `build.gradle`. > [_Turbo Documentation:_](https://github.com/hotwired/turbo-android#requirements) > -> Android SDK 26+ is required as the minSdkVersion in your build.gradle. +> Android SDK 28+ is required as the minSdkVersion in your build.gradle. -For iOS, you need to set the deployment target to 14.0 or higher. +For iOS, you need to set the deployment target to 15.6 or higher. ## Example diff --git a/packages/turbo/RNTurbo.podspec b/packages/turbo/RNHotwireNative.podspec similarity index 77% rename from packages/turbo/RNTurbo.podspec rename to packages/turbo/RNHotwireNative.podspec index c5d18d7b..8ff3329a 100644 --- a/packages/turbo/RNTurbo.podspec +++ b/packages/turbo/RNHotwireNative.podspec @@ -3,11 +3,11 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' -turbo_ios_source_files = "ios/vendor/turbo-ios/Source/**/*.{h,m,mm,swift}" -turbo_ios_resource_files = "ios/vendor/turbo-ios/Source/**/*.{js}" +hotwire_native_ios_source_files = "ios/vendor/hotwire-native-ios/Source/**/*.{h,m,mm,swift}" +hotwire_native_ios_resource_files = "ios/vendor/hotwire-native-ios/Source/**/*.{js}" Pod::Spec.new do |s| - s.name = "RNTurbo" + s.name = "RNHotwireNative" s.version = package["version"] s.summary = package["description"] s.homepage = package["homepage"] @@ -17,8 +17,8 @@ Pod::Spec.new do |s| s.platforms = { :ios => "14.0" } s.source = { :git => "https://github.com/software-mansion-labs/react-native-turbo-demo.git", :tag => "#{s.version}" } - s.source_files = "ios/*.{h,m,mm,swift}", turbo_ios_source_files - s.resource = turbo_ios_resource_files + s.source_files = "ios/*.{h,m,mm,swift}", hotwire_native_ios_source_files + s.resource = hotwire_native_ios_resource_files s.dependency "React-Core" diff --git a/packages/turbo/android/build.gradle b/packages/turbo/android/build.gradle index 30c902bb..4b046db2 100644 --- a/packages/turbo/android/build.gradle +++ b/packages/turbo/android/build.gradle @@ -1,8 +1,8 @@ -apply from: './turbo-android-dependencies.gradle' +apply from: './hotwire-native-android-dependencies.gradle' buildscript { // Buildscript is evaluated before everything else so we can't use getExtOrDefault - def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["TurboWebview_kotlinVersion"] + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["HotwireWebview_kotlinVersion"] repositories { google() @@ -28,11 +28,11 @@ if (isNewArchitectureEnabled()) { } def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["TurboWebview_" + name] + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["HotwireWebview_" + name] } def getExtOrIntegerDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TurboWebview_" + name]).toInteger() + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["HotwireWebview_" + name]).toInteger() } def supportsNamespace() { @@ -44,7 +44,7 @@ def supportsNamespace() { return (major == 7 && minor >= 3) || major >= 8 } -def addTurboDependencies(Map libraries) { +def addHotwireDependencies(Map libraries) { libraries.each { type, lib -> if (type.startsWith("dep")) { dependencies { @@ -68,9 +68,10 @@ allprojects { } } + android { if (supportsNamespace()) { - namespace "com.reactnativeturbowebview" + namespace "com.reactnativehotwirewebview" sourceSets { main { @@ -113,6 +114,7 @@ repositories { } } + def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { @@ -121,8 +123,9 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "dev.hotwire:turbo:local" + implementation "dev.hotwire:core:local" + implementation "dev.hotwire:navigation-fragments:local" } -addTurboDependencies(deps) -addTurboDependencies(apis) \ No newline at end of file +addHotwireDependencies(deps) +addHotwireDependencies(apis) \ No newline at end of file diff --git a/packages/turbo/android/gradle.properties b/packages/turbo/android/gradle.properties index 7787aca7..1809949a 100644 --- a/packages/turbo/android/gradle.properties +++ b/packages/turbo/android/gradle.properties @@ -1,5 +1,5 @@ -TurboWebview_kotlinVersion=1.9.10 -TurboWebview_minSdkVersion=26 -TurboWebview_targetSdkVersion=31 -TurboWebview_compileSdkVersion=31 -TurboWebview_ndkversion=21.4.7075529 +HotwireWebview_kotlinVersion=1.9.25 +HotwireWebview_minSdkVersion=26 +HotwireWebview_targetSdkVersion=31 +HotwireWebview_compileSdkVersion=31 +HotwireWebview_ndkversion=21.4.7075529 diff --git a/packages/turbo/android/src/main/AndroidManifest.xml b/packages/turbo/android/src/main/AndroidManifest.xml index 92270813..55f7a6a6 100644 --- a/packages/turbo/android/src/main/AndroidManifest.xml +++ b/packages/turbo/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.reactnativehotwirewebview"> diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNFileChooserDelegate.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNFileChooserDelegate.kt similarity index 99% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNFileChooserDelegate.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNFileChooserDelegate.kt index d4c735be..7f516278 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNFileChooserDelegate.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNFileChooserDelegate.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.Manifest import android.app.Activity diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSession.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSession.kt similarity index 73% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSession.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSession.kt index f4802235..859af6be 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSession.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSession.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.webkit.JavascriptInterface import android.webkit.WebSettings @@ -8,12 +8,17 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.whenStateAtLeast import com.facebook.react.bridge.ReactApplicationContext -import dev.hotwire.turbo.errors.TurboVisitError -import dev.hotwire.turbo.session.TurboSession -import dev.hotwire.turbo.views.TurboWebView -import dev.hotwire.turbo.visit.TurboVisit -import dev.hotwire.turbo.visit.TurboVisitAction -import dev.hotwire.turbo.visit.TurboVisitOptions +import com.reactnativehotwirewebview.RNWebChromeClient +import com.reactnativehotwirewebview.SessionCallbackAdapter +import com.reactnativehotwirewebview.SessionSubscriber +import com.reactnativehotwirewebview.Utils +import dev.hotwire.core.config.Hotwire +import dev.hotwire.core.turbo.errors.VisitError +import dev.hotwire.core.turbo.session.Session +import dev.hotwire.core.turbo.webview.HotwireWebView +import dev.hotwire.core.turbo.visit.Visit +import dev.hotwire.core.turbo.visit.VisitAction +import dev.hotwire.core.turbo.visit.VisitOptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -27,10 +32,10 @@ class RNSession( var visitableView: SessionSubscriber? = null - private val turboSession: TurboSession = run { + private val turboSession: Session = run { val activity = reactContext.currentActivity as AppCompatActivity - val webView = TurboWebView(activity, null) - val session = TurboSession(sessionHandle, activity, webView) + val webView = HotwireWebView(activity, null) + val session = Session(sessionHandle, activity, webView) webView.settings.setJavaScriptEnabled(true) webView.addJavascriptInterface(JavaScriptInterface(), "AndroidInterface") @@ -39,14 +44,14 @@ class RNSession( session.isRunningInAndroidNavigation = false session } - val webView: TurboWebView get() = turboSession.webView - val currentVisit: TurboVisit? get() = turboSession.currentVisit + val webView: HotwireWebView get() = turboSession.webView + val currentVisit: Visit? get() = turboSession.currentVisit internal fun registerVisitableView(newView: SessionSubscriber) { visitableView = newView } - private fun setUserAgentString(webView: TurboWebView, applicationNameForUserAgent: String?) { + private fun setUserAgentString(webView: HotwireWebView, applicationNameForUserAgent: String?) { var userAgentString = WebSettings.getDefaultUserAgent(webView.context) if (applicationNameForUserAgent != null) { userAgentString = "$userAgentString $applicationNameForUserAgent" @@ -54,23 +59,23 @@ class RNSession( webView.settings.userAgentString = userAgentString } - fun visit(url: String, restoreWithCachedSnapshot: Boolean, reload: Boolean, viewTreeLifecycleOwner: LifecycleOwner?, visitOptions: TurboVisitOptions?){ + fun visit(url: String, restoreWithCachedSnapshot: Boolean, reload: Boolean, viewTreeLifecycleOwner: LifecycleOwner?, visitOptions: VisitOptions?){ val restore = restoreWithCachedSnapshot && !reload val options = visitOptions ?: when { - restore -> TurboVisitOptions(action = TurboVisitAction.RESTORE) - else -> TurboVisitOptions() + restore -> VisitOptions(action = VisitAction.RESTORE) + else -> VisitOptions() } viewTreeLifecycleOwner?.lifecycleScope?.launch { val snapshot = when (options.action) { - TurboVisitAction.ADVANCE -> fetchCachedSnapshot(url) + VisitAction.ADVANCE -> fetchCachedSnapshot(url) else -> null } viewTreeLifecycleOwner.lifecycle.whenStateAtLeast(Lifecycle.State.STARTED) { turboSession.visit( - TurboVisit( + Visit( location = url, destinationIdentifier = url.hashCode(), restoreWithCachedSnapshot = restoreWithCachedSnapshot, @@ -85,7 +90,7 @@ class RNSession( private suspend fun fetchCachedSnapshot(url: String): String? { return withContext(Dispatchers.IO) { - val response = turboSession.offlineRequestHandler?.getCachedSnapshot( + val response = Hotwire.config.offlineRequestHandler?.getCachedSnapshot( url = url ) response?.data?.use { @@ -120,7 +125,7 @@ class RNSession( } fun clearSnapshotCache() { - // turbo-android doesn't expose a way to clear the snapshot cache, so we have to do it manually + // hotwire-native-android doesn't expose a way to clear the snapshot cache, so we have to do it manually webView.post { webView.evaluateJavascript("window.Turbo.session.clearCache();", null) } @@ -128,7 +133,7 @@ class RNSession( // region SessionCallbackAdapter - override fun onReceivedError(error: TurboVisitError) { + override fun onReceivedError(error: VisitError) { visitableView?.onReceivedError(error) } @@ -152,7 +157,11 @@ class RNSession( visitableView?.visitLocationStarted(location) } - override fun visitProposedToLocation(location: String, options: TurboVisitOptions) { + override fun visitProposedToCrossOriginRedirect(location: String) { + visitableView?.visitProposedToCrossOriginRedirect(location) + } + + override fun visitProposedToLocation(location: String, options: VisitOptions) { visitableView?.visitProposedToLocation(location, options) } @@ -168,7 +177,7 @@ class RNSession( visitableView?.didFinishFormSubmission(location) } - override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError) { + override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: VisitError) { visitableView?.requestFailedWithError(visitHasCachedSnapshot, error) } diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSessionManager.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSessionManager.kt similarity index 96% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSessionManager.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSessionManager.kt index ee0bf81d..f591d69a 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNSessionManager.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNSessionManager.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.Promise; import com.facebook.react.module.annotations.ReactModule +import com.reactnativehotwirewebview.RNSession private const val NAME = "RNSessionManager" diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNTurboError.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNTurboError.kt similarity index 73% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNTurboError.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNTurboError.kt index 1c25e439..8701c441 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNTurboError.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNTurboError.kt @@ -1,11 +1,11 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.webkit.WebViewClient -import dev.hotwire.turbo.errors.HttpError -import dev.hotwire.turbo.errors.LoadError -import dev.hotwire.turbo.errors.TurboVisitError -import dev.hotwire.turbo.errors.WebError -import dev.hotwire.turbo.errors.WebSslError +import dev.hotwire.core.turbo.errors.HttpError +import dev.hotwire.core.turbo.errors.LoadError +import dev.hotwire.core.turbo.errors.VisitError +import dev.hotwire.core.turbo.errors.WebError +import dev.hotwire.core.turbo.errors.WebSslError enum class RNTurboError(val code: Int) { HTTP(1), @@ -16,7 +16,7 @@ enum class RNTurboError(val code: Int) { UNKNOWN(-4); companion object { - fun getErrorCode(error: TurboVisitError): Int { + fun getErrorCode(error: VisitError): Int { val errorCode = when (error) { is HttpError -> error.statusCode is WebError -> error.errorCode @@ -28,7 +28,7 @@ enum class RNTurboError(val code: Int) { return when (errorCode) { WebViewClient.ERROR_CONNECT -> 0 WebViewClient.ERROR_TIMEOUT -> -1 - // turbo-android returns ERROR_UNKNOWN on SSL error and on turboFailedToLoad + // hotwire-native-android returns ERROR_UNKNOWN on SSL error and on turboFailedToLoad WebViewClient.ERROR_UNKNOWN -> -3 else -> if (errorCode > 0) errorCode else -4 } @@ -38,7 +38,7 @@ enum class RNTurboError(val code: Int) { return values().firstOrNull { it.code == code } ?: UNKNOWN } - fun errorDescription(error: TurboVisitError): String { + fun errorDescription(error: VisitError): String { val errorCode = getErrorCode(error) return when (fromCode(errorCode)) { NETWORK_FAILURE -> "A network error occurred." diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableView.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableView.kt similarity index 79% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableView.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableView.kt index 41b0d1ba..84413e48 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableView.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableView.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.content.Context import android.graphics.Bitmap @@ -10,16 +10,24 @@ import android.widget.LinearLayout import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.RCTEventEmitter -import dev.hotwire.turbo.views.TurboView -import dev.hotwire.turbo.views.TurboWebView -import dev.hotwire.turbo.visit.TurboVisitOptions -import dev.hotwire.turbo.R -import dev.hotwire.turbo.errors.TurboVisitError +import com.reactnativehotwirewebview.RNSessionManager +import com.reactnativehotwirewebview.RNTurboError +import com.reactnativehotwirewebview.RNVisitableViewEvent +import com.reactnativehotwirewebview.SessionSubscriber +import dev.hotwire.navigation.views.HotwireView +import dev.hotwire.core.turbo.webview.HotwireWebView +import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.navigation.R +import dev.hotwire.core.turbo.errors.VisitError +import dev.hotwire.navigation.util.HotwireViewScreenshotHolder +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking const val REFRESH_SCRIPT = "typeof Turbo.session.refresh === 'function'" + "? Turbo.session.refresh(document.baseURI)" + // Turbo 8+ @@ -58,7 +66,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib set(value) { field = value progressViewOffset?.let { - turboView.webViewRefresh?.setProgressViewOffset( + hotwireView.webViewRefresh?.setProgressViewOffset( it.getBoolean("scale"), it.getInt("start"), it.getInt("end") @@ -87,7 +95,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib return _session } - private val webView: TurboWebView? get() = session?.webView + private val webView: HotwireWebView? get() = session?.webView private var onConfirmHandler: ((result: Boolean) -> Unit)? = null private var onAlertHandler: (() -> Unit)? = null @@ -96,26 +104,22 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib private var isInitialVisit = true private var currentlyZoomed = false private var isWebViewAttachedToNewDestination = false - private var screenshotOrientation = 0 - private var screenshotZoomed = false - private var screenshot: Bitmap? = null + private val screenshotHolder = HotwireViewScreenshotHolder() // Views - private val visitableView = inflate(context, R.layout.turbo_view, null) as ViewGroup - private val turboView: TurboView by lazy { visitableView.findViewById(R.id.turbo_view) } - private val screenshotView: AppCompatImageView by lazy { visitableView.findViewById(R.id.turbo_screenshot) } - private val viewTreeLifecycleOwner get() = turboView.findViewTreeLifecycleOwner() + private val visitableView = inflate(context, R.layout.hotwire_view, null) as ViewGroup + private val hotwireView: HotwireView by lazy { visitableView.findViewById(R.id.hotwire_view) } + private val screenshotView: AppCompatImageView by lazy { visitableView.findViewById(R.id.hotwire_screenshot) } + private val viewTreeLifecycleOwner get() = hotwireView.findViewTreeLifecycleOwner() init { addView(visitableView) - turboView.apply { + hotwireView.apply { initializePullToRefresh(this) initializeErrorPullToRefresh(this) showScreenshotIfAvailable(this) - screenshot = null - screenshotOrientation = 0 - screenshotZoomed = false + screenshotHolder.reset() } } @@ -125,7 +129,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib setOnTouchListener(webView!!, scrollEnabled) } - private fun setOnTouchListener(webView: TurboWebView, scrollEnabled: Boolean) { + private fun setOnTouchListener(webView: HotwireWebView, scrollEnabled: Boolean) { if (!scrollEnabled) { webView.setOnTouchListener(OnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }) } else { @@ -168,7 +172,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib override fun reload(displayProgress: Boolean) { if (webView?.url == null) return - turboView.webViewRefresh?.apply { + hotwireView.webViewRefresh?.apply { if (displayProgress && !isRefreshing) { isRefreshing = true } @@ -178,26 +182,24 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib performVisit(restoreWithCachedSnapshot = false, reload = true) } - private fun initializePullToRefresh(turboView: TurboView) { - turboView.webViewRefresh?.apply { + private fun initializePullToRefresh(hotwireView: HotwireView) { + hotwireView.webViewRefresh?.apply { setOnRefreshListener { reload(displayProgress = true) } } } - private fun initializeErrorPullToRefresh(turboView: TurboView) { - turboView.errorRefresh?.apply { + private fun initializeErrorPullToRefresh(hotwireView: HotwireView) { + hotwireView.errorRefresh?.apply { setOnRefreshListener { reload(displayProgress = true) } } } - private fun showScreenshotIfAvailable(turboView: TurboView) { - if (screenshotOrientation == turboView.screenshotOrientation() && screenshotZoomed == currentlyZoomed) { - screenshot?.let { turboView.addScreenshot(it) } - } + private fun showScreenshotIfAvailable(hotwireView: HotwireView) { + screenshotHolder.showScreenshotIfAvailable(hotwireView) } // region view lifecycle @@ -218,10 +220,10 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib (webView!!.parent as ViewGroup).removeView(webView) } - // Re-layout the TurboView before attaching to make page restorations work correctly. + // Re-layout the HotwireView before attaching to make page restorations work correctly. requestLayout() - turboView.attachWebView(webView!!) { attachedToNewDestination -> + hotwireView.attachWebView(webView!!) { attachedToNewDestination -> onReady(attachedToNewDestination) } } @@ -231,14 +233,14 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib (webView!!.parent as ViewGroup?)?.endViewTransition(webView) - turboView.detachWebView(webView!!) { - // Force layout to fix improper layout of the TurboWebView. + hotwireView.detachWebView(webView!!) { + // Force layout to fix improper layout of the HotwireWebView. forceLayout() } } /* - * Fixes a bug in React Native, causing improper layout of the TurboView and its children. + * Fixes a bug in React Native, causing improper layout of the HotwireView and its children. * Refer to https://github.com/facebook/react-native/issues/17968 for the detailed issue discussion and context. */ override fun requestLayout() { @@ -265,11 +267,9 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib } private fun screenshotView() { - turboView.let { - screenshot = it.createScreenshot() - screenshotOrientation = it.screenshotOrientation() - screenshotZoomed = currentlyZoomed - showScreenshotIfAvailable(it) + viewTreeLifecycleOwner?.lifecycleScope?.launch { + screenshotHolder.captureScreenshot(hotwireView) + screenshotHolder.showScreenshotIfAvailable(hotwireView) } } @@ -284,15 +284,15 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib } private fun removeTransitionalViews() { - turboView.webViewRefresh?.isRefreshing = false - turboView.errorRefresh?.isRefreshing = false + hotwireView.webViewRefresh?.isRefreshing = false + hotwireView.errorRefresh?.isRefreshing = false hideProgressView() - turboView.removeScreenshot() - turboView.removeErrorView() + hotwireView.removeScreenshot() + hotwireView.removeErrorView() } private fun setPullToRefresh(enabled: Boolean) { - turboView.webViewRefresh?.isEnabled = enabled + hotwireView.webViewRefresh?.isEnabled = enabled } private fun sendEvent(event: RNVisitableViewEvent, params: WritableMap) { @@ -327,7 +327,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib }) } - override fun visitProposedToLocation(location: String, options: TurboVisitOptions) { + override fun visitProposedToLocation(location: String, options: VisitOptions) { sendEvent(RNVisitableViewEvent.VISIT_PROPOSAL, Arguments.createMap().apply { putString("url", location) putString("action", options.action.name.lowercase()) @@ -375,6 +375,12 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib } } + override fun visitProposedToCrossOriginRedirect(location: String) { + sendEvent(RNVisitableViewEvent.OPEN_EXTERNAL_URL, Arguments.createMap().apply { + putString("url", location) + }) + } + override fun handleAlert(message: String, callback: () -> Unit) { sendEvent(RNVisitableViewEvent.WEB_ALERT, Arguments.createMap().apply { putString("message", message) @@ -401,7 +407,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib onConfirmHandler = null } - override fun onReceivedError(error: TurboVisitError) { + override fun onReceivedError(error: VisitError) { sendEvent(RNVisitableViewEvent.ERROR, Arguments.createMap().apply { putInt("statusCode", RNTurboError.getErrorCode(error)) putString("url", url) @@ -409,7 +415,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib }) } - override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError) { + override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: VisitError) { sendEvent(RNVisitableViewEvent.ERROR, Arguments.createMap().apply { putInt("statusCode", RNTurboError.getErrorCode(error)) putString("url", url) diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableViewManager.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableViewManager.kt similarity index 97% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableViewManager.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableViewManager.kt index 1e24ef10..33c809af 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNVisitableViewManager.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNVisitableViewManager.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp +import com.reactnativehotwirewebview.RNVisitableView enum class RNVisitableViewEvent(val jsCallbackName: String) { VISIT_PROPOSAL("onVisitProposal"), diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebChromeClient.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebChromeClient.kt similarity index 96% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebChromeClient.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebChromeClient.kt index b6ad626f..cd32433d 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebChromeClient.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebChromeClient.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.app.Activity import android.content.Intent @@ -15,6 +15,7 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.RCTEventEmitter +import com.reactnativehotwirewebview.RNSession class RNWebChromeClient( diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebViewFileProvider.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebViewFileProvider.kt similarity index 88% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebViewFileProvider.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebViewFileProvider.kt index fa41a28a..74d5343f 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/RNWebViewFileProvider.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/RNWebViewFileProvider.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import androidx.core.content.FileProvider diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/ReactAppPackage.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/ReactAppPackage.kt similarity index 92% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/ReactAppPackage.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/ReactAppPackage.kt index f88daaa6..dd15c400 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/ReactAppPackage.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/ReactAppPackage.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionCallbackAdapter.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionCallbackAdapter.kt similarity index 54% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionCallbackAdapter.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionCallbackAdapter.kt index f9aa6376..2aad57ed 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionCallbackAdapter.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionCallbackAdapter.kt @@ -1,13 +1,13 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import android.webkit.HttpAuthHandler -import dev.hotwire.turbo.nav.TurboNavDestination -import dev.hotwire.turbo.session.TurboSessionCallback +import dev.hotwire.core.turbo.session.SessionCallback +import dev.hotwire.core.turbo.visit.VisitDestination -interface SessionCallbackAdapter : TurboSessionCallback { +interface SessionCallbackAdapter : SessionCallback { - override fun visitNavDestination(): TurboNavDestination { - throw Exception("Calling TurboNavDestination getter in ReactNative app") + override fun visitDestination(): VisitDestination { + throw Exception("Calling VisitDestination getter in ReactNative app") } override fun onPageFinished(location: String) {} diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionSubscriber.kt similarity index 93% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionSubscriber.kt index 63c52199..9f0cdf40 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/SessionSubscriber.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/SessionSubscriber.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import com.facebook.react.bridge.WritableMap diff --git a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/Utils.kt b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/Utils.kt similarity index 97% rename from packages/turbo/android/src/main/java/com/reactnativeturbowebview/Utils.kt rename to packages/turbo/android/src/main/java/com/reactnativehotwirewebview/Utils.kt index dd8b0092..04ed2b40 100644 --- a/packages/turbo/android/src/main/java/com/reactnativeturbowebview/Utils.kt +++ b/packages/turbo/android/src/main/java/com/reactnativehotwirewebview/Utils.kt @@ -1,4 +1,4 @@ -package com.reactnativeturbowebview +package com.reactnativehotwirewebview import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap diff --git a/packages/turbo/ios/RNSession.swift b/packages/turbo/ios/RNSession.swift index 54d8544d..789c8a98 100644 --- a/packages/turbo/ios/RNSession.swift +++ b/packages/turbo/ios/RNSession.swift @@ -97,6 +97,10 @@ extension RNSession: SessionDelegate { visitableView?.didProposeVisit(proposal: proposal) } + func session(_ session: Session, didProposeVisitToCrossOriginRedirect location: URL) { + visitableView?.didProposeVisitToCrossOriginRedirect(location: location) + } + func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) { visitableView?.didFailRequestForVisitable(visitable: visitable, error: error) } diff --git a/packages/turbo/ios/RNVisitableView.swift b/packages/turbo/ios/RNVisitableView.swift index a9cd4782..ed57ec1e 100644 --- a/packages/turbo/ios/RNVisitableView.swift +++ b/packages/turbo/ios/RNVisitableView.swift @@ -195,6 +195,10 @@ class RNVisitableView: UIView, RNSessionSubscriber { } } + public func didProposeVisitToCrossOriginRedirect(location: URL){ + onOpenExternalUrl?(["url": location.absoluteString]) + } + func getStatusCodeFromError(error: TurboError?) -> Int { switch error { case .networkFailure: diff --git a/packages/turbo/package.json b/packages/turbo/package.json index b47dcd53..f2a9eb34 100644 --- a/packages/turbo/package.json +++ b/packages/turbo/package.json @@ -21,12 +21,12 @@ "module": "lib/module/index.js", "types": "lib/typescript/src/index.d.ts", "react-native": "src/index.tsx", - "hotwiredTurbo": { - "ios": "7.1.0", - "android": "7.1.0" + "hotwireNative": { + "ios": "1.1.3", + "android": "1.1.1" }, "scripts": { - "build": "bob build && sh ./scripts/build.sh $npm_package_hotwiredTurbo_ios $npm_package_hotwiredTurbo_android && yarn link", + "build": "bob build && sh ./scripts/build.sh $npm_package_hotwireNative_ios $npm_package_hotwireNative_android && yarn link", "postinstall": "sh ./scripts/postinstall.sh", "clean": "rm -rf lib node_modules", "release": "sh ../../scripts/release.sh", @@ -34,8 +34,8 @@ "lint": "eslint \"src/**/*.{js,ts,tsx}\"" }, "devDependencies": { - "@react-navigation/core": "^6.0.0", - "@react-navigation/native": "^6.0.0", + "@react-navigation/core": ">=6.0.0", + "@react-navigation/native": ">=6.0.0", "react-native-safe-area-context": "5.2.0", "react-native-screens": "^4.10.0" }, @@ -53,7 +53,7 @@ "android", "ios", "scripts/postinstall.sh", - "RNTurbo.podspec", + "RNHotwireNative.podspec", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__" diff --git a/packages/turbo/patches/README.md b/packages/turbo/patches/README.md index c201e18e..6335426a 100644 --- a/packages/turbo/patches/README.md +++ b/packages/turbo/patches/README.md @@ -1,13 +1,13 @@ -# Patches for `turbo-ios` and `turbo-android` libraries +# Patches for `hotwire-native-ios` and `hotwire-native-android` libraries -This directory contains patches for the `turbo-ios` and `turbo-android` libraries. The patches are applied during the build process. Unfortunately, this process is necessary to make the libraries work correctly with `react-native-turbo`. +This directory contains patches for the `hotwire-native-ios` and `hotwire-native-android` libraries. The patches are applied during the build process. Unfortunately, this process is necessary to make the libraries work correctly with `react-native-turbo`. ## Changes -### `turbo-ios` +### `hotwire-native-ios` The patch removes the `NSLayoutConstraint` set in `VisitableView.installRefreshControl` method. This is necessary to make `contentInset` work properly with `UIRefreshControl`. -### `turbo-android` +### `hotwire-native-android` The patch makes the necessary interfaces, classes and methods public so that they can be accessed from the `react-native-turbo` native land. diff --git a/packages/turbo/patches/hotwire-native-android.patch b/packages/turbo/patches/hotwire-native-android.patch new file mode 100644 index 00000000..ec9b3eb8 --- /dev/null +++ b/packages/turbo/patches/hotwire-native-android.patch @@ -0,0 +1,79 @@ +diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt +index 6cf8bd1..255bd35 100644 +--- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt ++++ b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt +@@ -57,6 +57,7 @@ class Session( + internal val httpRepository = HttpRepository() + internal val offlineHttpRepository = OfflineHttpRepository(activity.lifecycleScope) + internal val offlineRequestInterceptor = OfflineWebViewRequestInterceptor(this) ++ var isRunningInAndroidNavigation = true + + // User accessible + +@@ -696,7 +697,7 @@ class Session( + private fun callback(action: (SessionCallback) -> Unit) { + context.runOnUiThread { + currentVisit?.callback?.let { callback -> +- if (callback.visitDestination().isActive()) { ++ if (!isRunningInAndroidNavigation || callback.visitDestination().isActive()) { + action(callback) + } + } +diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireViewScreenshotHolder.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireViewScreenshotHolder.kt +index d0beb31..ba36635 100644 +--- a/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireViewScreenshotHolder.kt ++++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/util/HotwireViewScreenshotHolder.kt +@@ -15,7 +15,7 @@ import dev.hotwire.navigation.views.HotwireView + import kotlinx.coroutines.ExperimentalCoroutinesApi + import kotlinx.coroutines.suspendCancellableCoroutine + +-internal class HotwireViewScreenshotHolder { ++class HotwireViewScreenshotHolder { + private var bitmap: Bitmap? = null + private var screenshotOrientation = 0 + private var screenshotZoomed = false +diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/views/HotwireView.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/HotwireView.kt +index f51c0ba..90735d9 100644 +--- a/navigation-fragments/src/main/java/dev/hotwire/navigation/views/HotwireView.kt ++++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/views/HotwireView.kt +@@ -25,10 +25,10 @@ class HotwireView @JvmOverloads constructor(context: Context, attrs: AttributeSe + private val errorContainer: ViewGroup get() = findViewById(R.id.hotwire_error_container) + private val screenshotView: ImageView get() = findViewById(R.id.hotwire_screenshot) + +- internal val webViewRefresh: SwipeRefreshLayout? get() = webViewContainer as? SwipeRefreshLayout +- internal val errorRefresh: SwipeRefreshLayout? get() = findViewById(R.id.hotwire_error_refresh) ++ val webViewRefresh: SwipeRefreshLayout? get() = webViewContainer as? SwipeRefreshLayout ++ val errorRefresh: SwipeRefreshLayout? get() = findViewById(R.id.hotwire_error_refresh) + +- internal fun attachWebView(webView: WebView, onAttachedToNewDestination: (Boolean) -> Unit) { ++ fun attachWebView(webView: WebView, onAttachedToNewDestination: (Boolean) -> Unit) { + if (webView.parent != null) { + onAttachedToNewDestination(false) + return +@@ -58,7 +58,7 @@ class HotwireView @JvmOverloads constructor(context: Context, attrs: AttributeSe + } + } + +- internal fun detachWebView(webView: WebView, onDetached: () -> Unit) { ++ fun detachWebView(webView: WebView, onDetached: () -> Unit) { + // If the view is already detached from the window (like + // when dismissing a bottom sheet), detach immediately, + // since posting to the message queue will be ignored. +@@ -100,7 +100,7 @@ class HotwireView @JvmOverloads constructor(context: Context, attrs: AttributeSe + screenshotView.isVisible = true + } + +- internal fun removeScreenshot() { ++ fun removeScreenshot() { + screenshotView.setImageBitmap(null) + screenshotView.isVisible = false + } +@@ -119,7 +119,7 @@ class HotwireView @JvmOverloads constructor(context: Context, attrs: AttributeSe + } + } + +- internal fun removeErrorView() { ++ fun removeErrorView() { + errorContainer.removeAllViews() + errorContainer.isVisible = false + diff --git a/packages/turbo/patches/hotwire-native-ios.patch b/packages/turbo/patches/hotwire-native-ios.patch new file mode 100644 index 00000000..d68069c2 --- /dev/null +++ b/packages/turbo/patches/hotwire-native-ios.patch @@ -0,0 +1,77 @@ +diff --git a/Source/Bridge/Bridge.swift b/Source/Bridge/Bridge.swift +index b454c43..1f2f616 100644 +--- a/Source/Bridge/Bridge.swift ++++ b/Source/Bridge/Bridge.swift +@@ -135,9 +135,17 @@ public final class Bridge: Bridgable { + configuration.userContentController.add(scriptMessageHandler, name: scriptHandlerName) + } + ++ private static var bundle: Bundle { ++ #if SWIFT_PACKAGE ++ return Bundle.module ++ #else ++ return Bundle(for: Bridge.self) ++ #endif ++ } ++ + private func makeUserScript() -> WKUserScript? { + guard +- let path = Bundle.module.path(forResource: "bridge", ofType: "js") ++ let path = Self.bundle.path(forResource: "bridge", ofType: "js") + else { + return nil + } +diff --git a/Source/Turbo/Visitable/VisitableView.swift b/Source/Turbo/Visitable/VisitableView.swift +index f0adc36..c1fd844 100644 +--- a/Source/Turbo/Visitable/VisitableView.swift ++++ b/Source/Turbo/Visitable/VisitableView.swift +@@ -16,6 +16,8 @@ open class VisitableView: UIView { + installActivityIndicatorView() + } + ++ public var refreshControlTopAnchor: CGFloat = 0 ++ + // MARK: Web View + + open var webView: WKWebView? +@@ -73,10 +75,17 @@ open class VisitableView: UIView { + /// Infer refresh control's default height from its frame, if given. + /// Otherwise fallback to 60 (the default height). + let refreshControlHeight = refreshControl.frame.height > 0 ? refreshControl.frame.height : 60 ++ let topAnchorConstraint: NSLayoutConstraint ++ ++ if (refreshControlTopAnchor > 0) { ++ topAnchorConstraint = refreshControl.topAnchor.constraint(equalTo: webView!.topAnchor, constant: refreshControlTopAnchor) ++ } else { ++ topAnchorConstraint = refreshControl.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor) ++ } + + NSLayoutConstraint.activate([ ++ topAnchorConstraint, + refreshControl.centerXAnchor.constraint(equalTo: centerXAnchor), +- refreshControl.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + refreshControl.heightAnchor.constraint(equalToConstant: refreshControlHeight) + ]) + #endif +diff --git a/Source/Turbo/WebView/WebViewBridge.swift b/Source/Turbo/WebView/WebViewBridge.swift +index 215a997..311b4be 100644 +--- a/Source/Turbo/WebView/WebViewBridge.swift ++++ b/Source/Turbo/WebView/WebViewBridge.swift +@@ -49,8 +49,16 @@ final class WebViewBridge { + webView.configuration.userContentController.add(ScriptMessageHandler(delegate: self), name: messageHandlerName) + } + ++ private static var bundle: Bundle { ++ #if SWIFT_PACKAGE ++ return Bundle.module ++ #else ++ return Bundle(for: WebViewBridge.self) ++ #endif ++ } ++ + private var userScript: WKUserScript { +- let url = Bundle.module.url(forResource: "turbo", withExtension: "js")! ++ let url = Self.bundle.url(forResource: "turbo", withExtension: "js")! + let source = try! String(contentsOf: url, encoding: .utf8) + return WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + } diff --git a/packages/turbo/patches/turbo-android.patch b/packages/turbo/patches/turbo-android.patch deleted file mode 100644 index b3cd1d75..00000000 --- a/packages/turbo/patches/turbo-android.patch +++ /dev/null @@ -1,167 +0,0 @@ -diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt -index 77a8b3c..c873aef 100644 ---- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt -+++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt -@@ -39,12 +39,13 @@ import java.util.* - * @property webView An instance of a [TurboWebView] to be shared/managed. - */ - @Suppress("unused") --class TurboSession internal constructor( -+class TurboSession constructor( - internal val sessionName: String, - private val activity: AppCompatActivity, - val webView: TurboWebView - ) { -- internal var currentVisit: TurboVisit? = null -+ var currentVisit: TurboVisit? = null -+ internal set - internal var coldBootVisitIdentifier = "" - internal var previousOverrideUrlTime = 0L - internal var isColdBooting = false -@@ -55,6 +56,7 @@ class TurboSession internal constructor( - internal val httpRepository = TurboHttpRepository(activity.lifecycleScope) - internal val requestInterceptor = TurboWebViewRequestInterceptor(this) - internal val fileChooserDelegate = TurboFileChooserDelegate(this) -+ var isRunningInAndroidNavigation = true - - // User accessible - -@@ -129,7 +131,7 @@ class TurboSession internal constructor( - - // Internal - -- internal fun visit(visit: TurboVisit) { -+ fun visit(visit: TurboVisit) { - this.currentVisit = visit - callback { it.visitLocationStarted(visit.location) } - -@@ -149,7 +151,7 @@ class TurboSession internal constructor( - * visit request. This is used when restoring a Fragment destination from the backstack, - * but the WebView's current location hasn't changed from the destination's location. - */ -- internal fun restoreCurrentVisit(callback: TurboSessionCallback): Boolean { -+ fun restoreCurrentVisit(callback: TurboSessionCallback): Boolean { - val visit = currentVisit ?: return false - val restorationIdentifier = restorationIdentifiers[visit.destinationIdentifier] - -@@ -627,7 +629,7 @@ class TurboSession internal constructor( - private fun callback(action: (TurboSessionCallback) -> Unit) { - context.runOnUiThread { - currentVisit?.callback?.let { callback -> -- if (callback.visitNavDestination().isActive) { -+ if (!isRunningInAndroidNavigation || callback.visitNavDestination().isActive) { - action(callback) - } - } -diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt -index 0bbaa01..7d8cc6c 100644 ---- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt -+++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt -@@ -5,7 +5,7 @@ import dev.hotwire.turbo.nav.TurboNavDestination - import dev.hotwire.turbo.errors.TurboVisitError - import dev.hotwire.turbo.visit.TurboVisitOptions - --internal interface TurboSessionCallback { -+interface TurboSessionCallback { - fun onPageStarted(location: String) - fun onPageFinished(location: String) - fun onReceivedError(error: TurboVisitError) -diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboView.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboView.kt -index bc0596f..7ca2621 100644 ---- a/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboView.kt -+++ b/turbo/src/main/kotlin/dev/hotwire/turbo/views/TurboView.kt -@@ -25,10 +25,10 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - private val errorContainer: ViewGroup get() = findViewById(R.id.turbo_error_container) - private val screenshotView: ImageView get() = findViewById(R.id.turbo_screenshot) - -- internal val webViewRefresh: SwipeRefreshLayout? get() = webViewContainer as? SwipeRefreshLayout -- internal val errorRefresh: SwipeRefreshLayout? get() = findViewById(R.id.turbo_error_refresh) -+ val webViewRefresh: SwipeRefreshLayout? get() = webViewContainer as? SwipeRefreshLayout -+ val errorRefresh: SwipeRefreshLayout? get() = findViewById(R.id.turbo_error_refresh) - -- internal fun attachWebView(webView: WebView, onAttachedToNewDestination: (Boolean) -> Unit) { -+ fun attachWebView(webView: WebView, onAttachedToNewDestination: (Boolean) -> Unit) { - if (webView.parent != null) { - onAttachedToNewDestination(false) - return -@@ -58,7 +58,7 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - } - } - -- internal fun detachWebView(webView: WebView, onDetached: () -> Unit) { -+ fun detachWebView(webView: WebView, onDetached: () -> Unit) { - // If the view is already detached from the window (like - // when dismissing a bottom sheet), detach immediately, - // since posting to the message queue will be ignored. -@@ -77,7 +77,7 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - return webViewContainer.contains(webView) - } - -- internal fun addProgressView(progressView: View) { -+ fun addProgressView(progressView: View) { - // Don't show the progress view if a screenshot is available - if (screenshotView.isVisible) return - -@@ -88,19 +88,19 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - progressContainer.isVisible = true - } - -- internal fun removeProgressView() { -+ fun removeProgressView() { - progressContainer.removeAllViews() - progressContainer.isVisible = false - } - -- internal fun addScreenshot(screenshot: Bitmap?) { -+ fun addScreenshot(screenshot: Bitmap?) { - if (screenshot == null) return - - screenshotView.setImageBitmap(screenshot) - screenshotView.isVisible = true - } - -- internal fun removeScreenshot() { -+ fun removeScreenshot() { - screenshotView.setImageBitmap(null) - screenshotView.isVisible = false - } -@@ -119,7 +119,7 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - } - } - -- internal fun removeErrorView() { -+ fun removeErrorView() { - errorContainer.removeAllViews() - errorContainer.isVisible = false - -@@ -130,7 +130,7 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - } - } - -- internal fun createScreenshot(): Bitmap? { -+ fun createScreenshot(): Bitmap? { - if (!isLaidOut) return null - if (!hasEnoughMemoryForScreenshot()) return null - if (width <= 0 || height <= 0) return null -@@ -143,7 +143,7 @@ class TurboView @JvmOverloads constructor(context: Context, attrs: AttributeSet? - } - } - -- internal fun screenshotOrientation(): Int { -+ fun screenshotOrientation(): Int { - return context.resources.configuration.orientation - } - -diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisit.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisit.kt -index f3f4504..bbb215a 100644 ---- a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisit.kt -+++ b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisit.kt -@@ -2,7 +2,7 @@ package dev.hotwire.turbo.visit - - import dev.hotwire.turbo.session.TurboSessionCallback - --internal data class TurboVisit( -+data class TurboVisit( - val location: String, - val destinationIdentifier: Int, - val restoreWithCachedSnapshot: Boolean, diff --git a/packages/turbo/patches/turbo-ios.patch b/packages/turbo/patches/turbo-ios.patch deleted file mode 100644 index 48a72f71..00000000 --- a/packages/turbo/patches/turbo-ios.patch +++ /dev/null @@ -1,32 +0,0 @@ -diff --git a/Source/Visitable/VisitableView.swift b/Source/Visitable/VisitableView.swift -index 12452b5..49b932c 100644 ---- a/Source/Visitable/VisitableView.swift -+++ b/Source/Visitable/VisitableView.swift -@@ -16,6 +16,8 @@ open class VisitableView: UIView { - installActivityIndicatorView() - } - -+ public var refreshControlTopAnchor: CGFloat = 0 -+ - // MARK: Web View - - open var webView: WKWebView? -@@ -74,9 +76,17 @@ open class VisitableView: UIView { - /// Otherwise fallback to 60 (the default height). - let refreshControlHeight = refreshControl.frame.height > 0 ? refreshControl.frame.height : 60 - -+ let topAnchorConstraint: NSLayoutConstraint -+ -+ if (refreshControlTopAnchor > 0) { -+ topAnchorConstraint = refreshControl.topAnchor.constraint(equalTo: webView!.topAnchor, constant: refreshControlTopAnchor) -+ } else { -+ topAnchorConstraint = refreshControl.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor) -+ } -+ - NSLayoutConstraint.activate([ -+ topAnchorConstraint, - refreshControl.centerXAnchor.constraint(equalTo: centerXAnchor), -- refreshControl.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - refreshControl.heightAnchor.constraint(equalToConstant: refreshControlHeight) - ]) - #endif diff --git a/packages/turbo/scripts/build-hotwire-native-android.sh b/packages/turbo/scripts/build-hotwire-native-android.sh new file mode 100755 index 00000000..e7b303db --- /dev/null +++ b/packages/turbo/scripts/build-hotwire-native-android.sh @@ -0,0 +1,53 @@ +HOTWIRE_NATIVE_ANDROID_REPO_PATH="https://github.com/hotwired/hotwire-native-android.git" +HOTWIRE_NATIVE_ANDROID_VERSION=$1 +PATCH_FILE=$(realpath ./patches/hotwire-native-android.patch) +HOTWIRE_NATIVE_ANDROID_DIR=$(realpath ./android) +DEPENDENCIES_GRADLE_FILE="hotwire-native-android-dependencies.gradle" +DEPENDENCY_REGEX="[a-zA-Z0-9.\-]+:[a-zA-Z0-9.\-]+:[0-9a-zA-Z.\-]+" + +# First argument is the version tag +if [ -z "$HOTWIRE_NATIVE_ANDROID_VERSION" ] + then + echo "No version tag supplied" + exit 1 +fi + +cd $HOTWIRE_NATIVE_ANDROID_DIR +rm -rf vendor +mkdir vendor + +# Shallow clone hotwire-native-android repo +rm -rf hotwire-native-android +git clone --branch $HOTWIRE_NATIVE_ANDROID_VERSION --depth 1 $HOTWIRE_NATIVE_ANDROID_REPO_PATH + +# Apply patch +cd hotwire-native-android +git apply $PATCH_FILE + +# Publish hotwire-native-android to local maven repository +./gradlew -PVersion=local publishToMavenLocal + +# Publish hotwire-native-android to vendor directory +./gradlew -Dmaven.repo.local=$HOTWIRE_NATIVE_ANDROID_DIR/vendor -PVersion=local publishToMavenLocal + +# Get necessary dependencies from hotwire-native-android build.gradle file +./gradlew core:dependencies --configuration implementation > core-deps.txt +./gradlew core:dependencies --configuration api > core-apis.txt +./gradlew navigation-fragments:dependencies --configuration implementation > nav-deps.txt +./gradlew navigation-fragments:dependencies --configuration api > nav-apis.txt + +# Get hotwire-native-android dependencies and create DEPENDENCIES_GRADLE_FILE +cd .. +rm -rf $DEPENDENCIES_GRADLE_FILE +touch $DEPENDENCIES_GRADLE_FILE + +echo "ext {\n\tdeps = [ " > $DEPENDENCIES_GRADLE_FILE +grep -oE $DEPENDENCY_REGEX ./hotwire-native-android/core-deps.txt | awk '{print "\t\tdepcore" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE +grep -oE $DEPENDENCY_REGEX ./hotwire-native-android/nav-deps.txt | awk '{print "\t\tdepnav" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE +echo "\t]\n\tapis = [" >> $DEPENDENCIES_GRADLE_FILE +grep -oE $DEPENDENCY_REGEX ./hotwire-native-android/core-apis.txt | awk '{print "\t\tapicore" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE +grep -oE $DEPENDENCY_REGEX ./hotwire-native-android/nav-apis.txt | awk '{print "\t\tapinav" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE +echo "\t] \n}" >> $DEPENDENCIES_GRADLE_FILE + +# Cleanup +rm -rf hotwire-native-android diff --git a/packages/turbo/scripts/build-hotwire-native-ios.sh b/packages/turbo/scripts/build-hotwire-native-ios.sh new file mode 100644 index 00000000..4a144b9f --- /dev/null +++ b/packages/turbo/scripts/build-hotwire-native-ios.sh @@ -0,0 +1,30 @@ +HOTWIRE_NATIVE_IOS_REPO_PATH="https://github.com/hotwired/hotwire-native-ios.git" +HOTWIRE_NATIVE_IOS_VERSION=$1 +PATCH_FILE=$(realpath ./patches/hotwire-native-ios.patch) + +# First argument is the version tag +if [ -z "$HOTWIRE_NATIVE_IOS_VERSION" ] + then + echo "No version tag supplied" + exit 1 +fi + +echo "hotwire-native-ios version: $HOTWIRE_NATIVE_IOS_VERSION" + +# Shallow clone the hotwire-native-ios repo +cd ./ios +rm -rf vendor +mkdir vendor +cd vendor +git clone --branch $HOTWIRE_NATIVE_IOS_VERSION --depth 1 $HOTWIRE_NATIVE_IOS_REPO_PATH + +# Apply patch +cd hotwire-native-ios +git apply $PATCH_FILE + +# Keep the Source folder and remove the rest +for file in *; do + if [ "$file" != "Source" ]; then + rm -rf $file + fi +done diff --git a/packages/turbo/scripts/build-turbo-android.sh b/packages/turbo/scripts/build-turbo-android.sh deleted file mode 100755 index 3c004289..00000000 --- a/packages/turbo/scripts/build-turbo-android.sh +++ /dev/null @@ -1,50 +0,0 @@ -TURBO_ANDROID_REPO_PATH="https://github.com/hotwired/turbo-android.git" -TURBO_ANDROID_MAIN_SOURCE_DIR="./turbo/src/main/*" -TURBO_ANDROID_VERSION=$1 -PATCH_FILE=$(realpath ./patches/turbo-android.patch) -TURBO_ANDROID_DIR=$(realpath ./android) -DEPENDENCIES_GRADLE_FILE="turbo-android-dependencies.gradle" -DEPENDENCY_REGEX="[a-zA-Z0-9.\-]+:[a-zA-Z0-9.\-]+:[0-9a-zA-Z.\-]+" - -# First argument is the version tag -if [ -z "$TURBO_ANDROID_VERSION" ] - then - echo "No version tag supplied" - exit 1 -fi - -cd $TURBO_ANDROID_DIR -rm -rf vendor -mkdir vendor - -# Shallow clone the turbo-ios repo -rm -rf turbo-android -git clone --branch $TURBO_ANDROID_VERSION --depth 1 $TURBO_ANDROID_REPO_PATH - -# Apply patch -cd turbo-android -git apply $PATCH_FILE - -# Publish turbo-android to local maven repository -./gradlew -PVersion=local publishToMavenLocal - -# Publish turbo-android to vendor directory -./gradlew -Dmaven.repo.local=$TURBO_ANDROID_DIR/vendor -PVersion=local publishToMavenLocal - -# Get necessary dependencies from turbo-android build.gradle file -./gradlew turbo:dependencies --configuration implementation > deps.txt -./gradlew turbo:dependencies --configuration api > apis.txt - -# Get turbo-android dependencies and create DEPENDENCIES_GRADLE_FILE -cd .. -rm -rf $DEPENDENCIES_GRADLE_FILE -touch $DEPENDENCIES_GRADLE_FILE - -echo "ext {\n\tdeps = [ " > $DEPENDENCIES_GRADLE_FILE -grep -oE $DEPENDENCY_REGEX ./turbo-android/deps.txt | awk '{print "\t\tdep" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE -echo "\t]\n\tapis = [" >> $DEPENDENCIES_GRADLE_FILE -grep -oE $DEPENDENCY_REGEX ./turbo-android/apis.txt | awk '{print "\t\tapi" NR ": " "\""$1"\"" ","}' >> $DEPENDENCIES_GRADLE_FILE -echo "\t] \n}" >> $DEPENDENCIES_GRADLE_FILE - -# Cleanup -rm -rf turbo-android diff --git a/packages/turbo/scripts/build-turbo-ios.sh b/packages/turbo/scripts/build-turbo-ios.sh deleted file mode 100644 index 03cedd80..00000000 --- a/packages/turbo/scripts/build-turbo-ios.sh +++ /dev/null @@ -1,30 +0,0 @@ -TURBO_IOS_REPO_PATH="https://github.com/hotwired/turbo-ios.git" -TURBO_IOS_VERSION=$1 -PATCH_FILE=$(realpath ./patches/turbo-ios.patch) - -# First argument is the version tag -if [ -z "$TURBO_IOS_VERSION" ] - then - echo "No version tag supplied" - exit 1 -fi - -echo "turbo-ios version: $TURBO_IOS_VERSION" - -# Shallow clone the turbo-ios repo -cd ./ios -rm -rf vendor -mkdir vendor -cd vendor -git clone --branch $TURBO_IOS_VERSION --depth 1 $TURBO_IOS_REPO_PATH - -# Apply patch -cd turbo-ios -git apply $PATCH_FILE - -# Keep the Source folder and remove the rest -for file in *; do - if [ "$file" != "Source" ]; then - rm -rf $file - fi -done diff --git a/packages/turbo/scripts/build.sh b/packages/turbo/scripts/build.sh index 1541c0fa..14aea098 100644 --- a/packages/turbo/scripts/build.sh +++ b/packages/turbo/scripts/build.sh @@ -1,18 +1,18 @@ -# Build the turbo-ios and turbo-android packages +# Build the hotwire-native-ios and hotwire-native-android packages -TURBO_IOS_VERSION=$1 -TURBO_ANDROID_VERSION=$2 +HOTWIRE_NATIVE_IOS_VERSION=$1 +HOTWIRE_NATIVE_ANDROID_VERSION=$2 -if [ -z "$TURBO_IOS_VERSION" ] +if [ -z "$HOTWIRE_NATIVE_IOS_VERSION" ] then - echo "No turbo-ios version tag supplied" + echo "No hotwire-native-ios version tag supplied" exit 1 fi -if [ -z "$TURBO_ANDROID_VERSION" ] +if [ -z "$HOTWIRE_NATIVE_ANDROID_VERSION" ] then - echo "No turbo-android version tag supplied" + echo "No hotwire-native-android version tag supplied" exit 1 fi -sh ./scripts/build-turbo-ios.sh $TURBO_IOS_VERSION & sh ./scripts/build-turbo-android.sh $TURBO_ANDROID_VERSION +sh ./scripts/build-hotwire-native-ios.sh $HOTWIRE_NATIVE_IOS_VERSION & sh ./scripts/build-hotwire-native-android.sh $HOTWIRE_NATIVE_ANDROID_VERSION diff --git a/yarn.lock b/yarn.lock index aba1aabd..39f1c98e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,7 +2714,20 @@ "@react-navigation/elements" "^2.3.1" color "^4.2.3" -"@react-navigation/core@^6.0.0", "@react-navigation/core@^6.4.17": +"@react-navigation/core@>=6.0.0", "@react-navigation/core@^7.8.5": + version "7.8.5" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.8.5.tgz#3ea29c75da1e5e6552b3be91a085092eddf6be83" + integrity sha512-xDUXs6NI6ASiZgf53I7NPG0iJVGClPL5O3r8ddOCkS6fhVmPRun64m2zxUWnPcxtheFNTFfQ1IXH+gcenTcv/w== + dependencies: + "@react-navigation/routers" "^7.3.5" + escape-string-regexp "^4.0.0" + nanoid "3.3.8" + query-string "^7.1.3" + react-is "^18.2.0" + use-latest-callback "^0.2.1" + use-sync-external-store "^1.2.2" + +"@react-navigation/core@^6.0.0": version "6.4.17" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.17.tgz#f277a196b578c8a456efcc563d1c9bd87eb4ab04" integrity sha512-Nd76EpomzChWAosGqWOYE3ItayhDzIEzzZsT7PfGcRFDgW5miHV2t4MZcq9YIK4tzxZjVVpYbIynOOQQd1e0Cg== @@ -2754,15 +2767,16 @@ "@react-navigation/elements" "^2.3.1" warn-once "^0.1.1" -"@react-navigation/native@^6.0.0": - version "6.1.18" - resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.1.18.tgz#338fa9afa2c89feec1d3eac41c963840d8d6f106" - integrity sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg== +"@react-navigation/native@>=6.0.0": + version "7.1.6" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.6.tgz#0b51e543ad37348000f856959faf295aa129cd93" + integrity sha512-XcfygfHDfAgf2iC4rNBc67Yy0M1aYRGNeNKqja5AJPFZoBQhAEAxKCwHsH4g3qU0zIbzLCthoSl5107dBjoeZw== dependencies: - "@react-navigation/core" "^6.4.17" + "@react-navigation/core" "^7.8.5" escape-string-regexp "^4.0.0" fast-deep-equal "^3.1.3" - nanoid "^3.1.23" + nanoid "3.3.8" + use-latest-callback "^0.2.1" "@react-navigation/native@^7.0.19": version "7.0.19" @@ -2789,6 +2803,13 @@ dependencies: nanoid "3.3.8" +"@react-navigation/routers@^7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.3.5.tgz#91673f18000d81106e401982c5f33a0f65e95fca" + integrity sha512-SBh/3G7pURIQfIwG4OnAfLvq0E4+l1Ii6577z22cIhWIrTOHFXg0rMxC7ft/amzxYn+iG2nYa4dONRd+xIs+yg== + dependencies: + nanoid "3.3.8" + "@release-it/bumper@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@release-it/bumper/-/bumper-6.0.1.tgz" @@ -8937,10 +8958,10 @@ react-native-turbo@*: integrity sha512-l7WcDaVTQD6S611MmFwvdgQawAACs43SVDKUs92pqBCQw3j7xXfW4bc5OoiIG6c/5qSsb3DNQj5XC6MnlkLPqQ== "react-native-turbo@file:packages/turbo": - version "1.0.0-beta.7" + version "1.1.0" "react-native-web-screen@file:packages/navigation": - version "1.0.0-beta.3" + version "1.1.0" react-native-web@~0.19.13: version "0.19.13"