Skip to content

Commit

Permalink
NTV-350 :Add Save Project deep link (#1540)
Browse files Browse the repository at this point in the history
* Add deeplink to project paged activity

* Add isProjectSaveUri to open project page

* Add the deeplink

* Add save from deeplink to project page and project activity

* Handle url deeplinking to activity
  • Loading branch information
hadia committed Feb 10, 2022
1 parent 9d3f450 commit b90e63d
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 5 deletions.
15 changes: 14 additions & 1 deletion app/src/main/AndroidManifest.xml
Expand Up @@ -182,9 +182,22 @@
android:parentActivityName=".ui.activities.DiscoveryActivity"
android:theme="@style/ProjectActivity"
android:screenOrientation="portrait"
android:exported="true"
android:configChanges="screenSize|orientation"
android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity" />
tools:ignore="LockedOrientationActivity" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="*.kickstarter.com"
android:pathPrefix="/projects/"
android:scheme="ksr" />
</intent-filter>
</activity>
<activity
android:name=".ui.activities.ProjectNotificationSettingsActivity"
android:parentActivityName=".ui.activities.SettingsActivity"
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/kickstarter/libs/utils/UrlUtils.kt
Expand Up @@ -10,6 +10,7 @@ object UrlUtils {

private const val KEY_REF = "ref"
private const val KEY_COMMENT = "comment"
private const val KEY_SAVE = "save"

fun appendPath(baseUrl: String, path: String): String {
val uriBuilder = Uri.parse(baseUrl).buildUpon()
Expand Down Expand Up @@ -45,4 +46,8 @@ object UrlUtils {
fun commentId(url: String): String? {
return Uri.parse(url).getQueryParameter(KEY_COMMENT)
}

fun saveFlag(url: String): Boolean? {
return Uri.parse(url).getQueryParameter(KEY_SAVE)?.equals("true")
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/kickstarter/libs/utils/extensions/UriExt.kt
Expand Up @@ -135,6 +135,12 @@ fun Uri.isProjectCommentUri(webEndpoint: String): Boolean {
.matches()
}

fun Uri.isProjectSaveUri(webEndpoint: String): Boolean {
return isKickstarterUri(webEndpoint) &&
PROJECT_PATTERN.matcher(path()).matches() &&
PROJECT_SAVE_QUERY_PATTERN.matcher(query()).matches()
}

fun Uri.isProjectUpdateCommentsUri(webEndpoint: String): Boolean {
return isKickstarterUri(webEndpoint) && PROJECT_UPDATE_COMMENTS_PATTERN.matcher(path()).matches()
}
Expand Down Expand Up @@ -212,6 +218,11 @@ private val PROJECT_COMMENTS_PATTERN = Pattern.compile(
"\\A\\/projects(\\/[a-zA-Z0-9_-]+)?\\/[a-zA-Z0-9_-]+\\/comments\\z"
)

// save=true|false
private val PROJECT_SAVE_QUERY_PATTERN = Pattern.compile(
"save(\\=[a-zA-Z]+)"
)

// /projects/:creator_param/:project_param/posts/:update_param/comments
private val PROJECT_UPDATE_COMMENTS_PATTERN = Pattern.compile(
"\\A\\/projects(\\/[a-zA-Z0-9_-]+)?\\/[a-zA-Z0-9_-]+\\/posts\\/[a-zA-Z0-9-_]+\\/comments\\z"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/kickstarter/ui/IntentKey.java
Expand Up @@ -44,6 +44,8 @@ private IntentKey() {}
public static final String REPLY_EXPAND = "com.kickstarter.kickstarter.intent_reply_expand";
public static final String REPLY_SCROLL_BOTTOM = "com.kickstarter.kickstarter.intent_reply_scroll_bottom";
public static final String DEEP_LINK_SCREEN_PROJECT_COMMENT = "com.kickstarter.kickstarter.deep_link_screen_project_comment";
public static final String DEEP_LINK_SCREEN_PROJECT_SAVE = "com.kickstarter.kickstarter.save";
public static final String SAVE_FLAG_VALUE = "com.kickstarter.kickstarter.intent_save_flag_value";
public static final String DEEP_LINK_SCREEN_PROJECT_UPDATE = "com.kickstarter.kickstarter.deep_link_screen_project_update";
public static final String DEEP_LINK_SCREEN_PROJECT_UPDATE_COMMENT = "com.kickstarter.kickstarter.deep_link_screen_project_update_comment";
public static final String VIDEO_SEEK_POSITION = "com.kickstarter.kickstarter.intent_video_seek_position";
Expand Down
Expand Up @@ -10,6 +10,8 @@ import com.kickstarter.libs.rx.transformers.Transformers
import com.kickstarter.libs.utils.ApplicationUtils
import com.kickstarter.libs.utils.UrlUtils.commentId
import com.kickstarter.libs.utils.UrlUtils.refTag
import com.kickstarter.libs.utils.UrlUtils.saveFlag
import com.kickstarter.libs.utils.extensions.getProjectIntent
import com.kickstarter.libs.utils.extensions.path
import com.kickstarter.ui.IntentKey
import com.kickstarter.viewmodels.DeepLinkViewModel
Expand Down Expand Up @@ -37,6 +39,11 @@ class DeepLinkActivity : BaseActivity<DeepLinkViewModel.ViewModel?>() {
.compose(Transformers.observeForUI())
.subscribe { uri: Uri -> startProjectActivity(uri) }

viewModel.outputs.startProjectActivityToSave()
.compose(bindToLifecycle())
.compose(Transformers.observeForUI())
.subscribe { startProjectActivityForSave(it.first, it.second) }

viewModel.outputs.startProjectActivityForComment()
.compose(bindToLifecycle())
.compose(Transformers.observeForUI())
Expand Down Expand Up @@ -87,6 +94,19 @@ class DeepLinkActivity : BaseActivity<DeepLinkViewModel.ViewModel?>() {
finish()
}

private fun startProjectActivityForSave(uri: Uri, isFfEnabled: Boolean) {
val projectIntent = Intent().getProjectIntent(this, isFfEnabled)
.setData(uri)
.putExtra(IntentKey.DEEP_LINK_SCREEN_PROJECT_SAVE, true)

saveFlag(uri.toString())?.let {
projectIntent.putExtra(IntentKey.SAVE_FLAG_VALUE, it)
}

startActivity(projectIntent)
finish()
}

private fun startProjectActivityForComment(uri: Uri) {
val projectIntent = projectIntent(uri)
.putExtra(IntentKey.DEEP_LINK_SCREEN_PROJECT_COMMENT, true)
Expand Down
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent
import android.net.Uri
import com.kickstarter.libs.RefTag
import com.kickstarter.libs.utils.ObjectUtils
import com.kickstarter.libs.utils.extensions.query
import com.kickstarter.models.Project
import com.kickstarter.services.ApiClientType
import com.kickstarter.services.ApolloClientType
Expand All @@ -20,6 +21,10 @@ object ProjectIntentMapper {
val PROJECT_PATTERN =
Pattern.compile("\\A\\/projects\\/([a-zA-Z0-9_-]+)(\\/([a-zA-Z0-9_-]+)).*")

private val PROJECT_SAVE_QUERY_PATTERN = Pattern.compile(
"save(\\=[a-zA-Z]+)"
)

fun project(intent: Intent, apolloClient: ApolloClientType): Observable<Project> {
val intentProject = projectFromIntent(intent)
val projectFromParceledProject =
Expand Down Expand Up @@ -71,6 +76,13 @@ object ProjectIntentMapper {
return Observable.just(intent.getParcelableExtra(IntentKey.REF_TAG))
}

/**
* Returns a [deepLinkSaveFlag] observable. If there is no deepLink Save Flag
*/
fun deepLinkSaveFlag(intent: Intent): Observable<Boolean> {
return Observable.just(intent.getBooleanExtra(IntentKey.SAVE_FLAG_VALUE, false))
}

/**
* Returns an observable of push notification envelopes from the intent data. This will emit only when the project
* is launched from a push notification.
Expand Down Expand Up @@ -113,4 +125,21 @@ object ProjectIntentMapper {
matcher.group(3)
} else null
}

/**
* check the project save query from a uri. e.g.: A uri like `ksr://www.kickstarter.com/projects/1186238668/skull-graphic-tee?save=true`
* returns true or false
*/
fun hasSaveQueryFromUri(uri: Uri?): Boolean {
if (uri == null) {
return false
}
val scheme = uri.scheme
if (!(scheme == SCHEME_KSR || scheme == SCHEME_HTTPS)) {
return false
}
val matcher = PROJECT_PATTERN.matcher(uri.path)
val query = PROJECT_SAVE_QUERY_PATTERN.matcher(uri.query()).matches()
return matcher.matches() && matcher.group(3) != null && query
}
}
37 changes: 37 additions & 0 deletions app/src/main/java/com/kickstarter/viewmodels/DeepLinkViewModel.kt
Expand Up @@ -7,7 +7,9 @@ import android.util.Pair
import com.kickstarter.libs.ActivityViewModel
import com.kickstarter.libs.CurrentUserType
import com.kickstarter.libs.Environment
import com.kickstarter.libs.ExperimentsClientType
import com.kickstarter.libs.RefTag
import com.kickstarter.libs.models.OptimizelyFeature
import com.kickstarter.libs.rx.transformers.Transformers
import com.kickstarter.libs.utils.ObjectUtils
import com.kickstarter.libs.utils.UrlUtils.appendRefTag
Expand All @@ -16,6 +18,7 @@ import com.kickstarter.libs.utils.extensions.canUpdateFulfillment
import com.kickstarter.libs.utils.extensions.isCheckoutUri
import com.kickstarter.libs.utils.extensions.isProjectCommentUri
import com.kickstarter.libs.utils.extensions.isProjectPreviewUri
import com.kickstarter.libs.utils.extensions.isProjectSaveUri
import com.kickstarter.libs.utils.extensions.isProjectUpdateCommentsUri
import com.kickstarter.libs.utils.extensions.isProjectUpdateUri
import com.kickstarter.libs.utils.extensions.isProjectUri
Expand Down Expand Up @@ -55,6 +58,9 @@ interface DeepLinkViewModel {

/** Emits when we should finish the current activity */
fun finishDeeplinkActivity(): Observable<Void>

/** Emits when we should start the [com.kickstarter.ui.activities.ProjectPageActivity]. */
fun startProjectActivityToSave(): Observable<Pair<Uri, Boolean>>
}

class ViewModel(environment: Environment) :
Expand All @@ -67,17 +73,26 @@ interface DeepLinkViewModel {
private val startProjectActivityForUpdate = BehaviorSubject.create<Uri>()
private val startProjectActivityForCommentToUpdate = BehaviorSubject.create<Uri>()
private val startProjectActivityWithCheckout = BehaviorSubject.create<Uri>()
private val startProjectActivityToSave = BehaviorSubject.create<Pair<Uri, Boolean>>()
private val updateUserPreferences = BehaviorSubject.create<Boolean>()
private val finishDeeplinkActivity = BehaviorSubject.create<Void?>()
private val apolloClient = environment.apolloClient()
private val apiClientType = environment.apiClient()
private val currentUser = environment.currentUser()
private val webEndpoint = environment.webEndpoint()
private val projectObservable: Observable<Project>
private val optimizely: ExperimentsClientType = environment.optimizely()

val outputs: Outputs = this

init {

val isProjectPageEnabled = Observable.just(
optimizely.isFeatureEnabled(
OptimizelyFeature.Key.PROJECT_PAGE_V2
)
)

val uriFromIntent = intent()
.map { obj: Intent -> obj.data }
.ofType(Uri::class.java)
Expand All @@ -95,6 +110,9 @@ interface DeepLinkViewModel {
.filter {
it.isProjectUri(webEndpoint)
}
.filter {
!it.isProjectSaveUri(webEndpoint)
}
.filter {
!it.isCheckoutUri(webEndpoint)
}
Expand All @@ -119,6 +137,22 @@ interface DeepLinkViewModel {
startProjectActivity.onNext(it)
}

uriFromIntent
.filter { ObjectUtils.isNotNull(it) }
.filter {
it.isProjectUri(webEndpoint)
}
.filter {
it.isProjectSaveUri(webEndpoint)
}.map { appendRefTagIfNone(it) }
.withLatestFrom(isProjectPageEnabled) { a, b ->
Pair.create(a, b)
}
.compose(bindToLifecycle())
.subscribe {
startProjectActivityToSave.onNext(it)
}

uriFromIntent
.filter { ObjectUtils.isNotNull(it) }
.filter {
Expand Down Expand Up @@ -228,6 +262,7 @@ interface DeepLinkViewModel {
val unsupportedDeepLink = uriFromIntent
.filter { !lastPathSegmentIsProjects(it) }
.filter { !it.isSettingsUrl() }
.filter { !it.isProjectSaveUri(webEndpoint) }
.filter { !it.isCheckoutUri(webEndpoint) }
.filter { !it.isProjectUri(webEndpoint) }
.filter { !it.isProjectCommentUri(webEndpoint) }
Expand Down Expand Up @@ -296,5 +331,7 @@ interface DeepLinkViewModel {
override fun startProjectActivityForCheckout(): Observable<Uri> = startProjectActivityWithCheckout

override fun finishDeeplinkActivity(): Observable<Void> = finishDeeplinkActivity

override fun startProjectActivityToSave(): Observable<Pair<Uri, Boolean>> = startProjectActivityToSave
}
}
42 changes: 40 additions & 2 deletions app/src/main/java/com/kickstarter/viewmodels/ProjectViewModel.kt
Expand Up @@ -429,6 +429,43 @@ interface ProjectViewModel {
}
.share()

val saveProjectFromDeepLinkActivity = intent()
.take(1)
.delay(3, TimeUnit.SECONDS, environment.scheduler()) // add delay to wait until activity subscribed to viewmodel
.filter {
it.getBooleanExtra(IntentKey.DEEP_LINK_SCREEN_PROJECT_SAVE, false)
}
.flatMap { ProjectIntentMapper.deepLinkSaveFlag(it) }

val saveProjectFromDeepUrl = intent()
.take(1)
.delay(3, TimeUnit.SECONDS, environment.scheduler()) // add delay to wait until activity subscribed to viewmodel
.filter { ObjectUtils.isNotNull(it.data) }
.map { requireNotNull(it.data) }
.filter {
ProjectIntentMapper.hasSaveQueryFromUri(it)
}
.map { UrlUtils.saveFlag(it.toString()) }
.filter { ObjectUtils.isNotNull(it) }
.map { requireNotNull(it) }

val saveProjectFromDeepLink = Observable.merge(saveProjectFromDeepLinkActivity, saveProjectFromDeepUrl)
.compose(combineLatestPair(this.currentUser.observable()))
.filter { it.second != null }
.withLatestFrom(initialProject) { userAndFlag, p ->
Pair(userAndFlag, p)
}
.take(1)
.filter {
it.second.isStarred() != it.first.first
}.switchMap {
if (it.first.first) {
this.saveProject(it.second)
} else {
this.unSaveProject(it.second)
}
}.share()

val refreshProjectEvent = Observable.merge(
this.pledgeSuccessfullyCancelled,
this.pledgeSuccessfullyCreated.compose(ignoreValues()),
Expand Down Expand Up @@ -474,10 +511,11 @@ interface ProjectViewModel {
initialProject,
refreshedProjectNotification.compose(values()),
projectOnUserChangeSave,
savedProjectOnLoginSuccess
savedProjectOnLoginSuccess,
saveProjectFromDeepLink
)

val projectSavedStatus = projectOnUserChangeSave.mergeWith(savedProjectOnLoginSuccess)
val projectSavedStatus = Observable.merge(projectOnUserChangeSave, savedProjectOnLoginSuccess, saveProjectFromDeepLink)

projectSavedStatus
.compose(bindToLifecycle())
Expand Down
Expand Up @@ -401,6 +401,26 @@ interface ProjectPageViewModel {
val refTag = intent()
.flatMap { ProjectIntentMapper.refTag(it) }

val saveProjectFromDeepLinkActivity = intent()
.take(1)
.delay(3, TimeUnit.SECONDS, environment.scheduler()) // add delay to wait until activity subscribed to viewmodel
.filter {
it.getBooleanExtra(IntentKey.DEEP_LINK_SCREEN_PROJECT_SAVE, false)
}
.flatMap { ProjectIntentMapper.deepLinkSaveFlag(it) }

val saveProjectFromDeepUrl = intent()
.take(1)
.delay(3, TimeUnit.SECONDS, environment.scheduler()) // add delay to wait until activity subscribed to viewmodel
.filter { ObjectUtils.isNotNull(it.data) }
.map { requireNotNull(it.data) }
.filter {
ProjectIntentMapper.hasSaveQueryFromUri(it)
}
.map { UrlUtils.saveFlag(it.toString()) }
.filter { ObjectUtils.isNotNull(it) }
.map { requireNotNull(it) }

val loggedInUserOnHeartClick = this.currentUser.observable()
.compose<User>(takeWhen(this.heartButtonClicked))
.filter { u -> u != null }
Expand Down Expand Up @@ -463,14 +483,32 @@ interface ProjectPageViewModel {
}
.share()

val projectOnDeepLinkChangeSave = Observable.merge(saveProjectFromDeepLinkActivity, saveProjectFromDeepUrl)
.compose(combineLatestPair(this.currentUser.observable()))
.filter { it.second != null }
.withLatestFrom(initialProject) { userAndFlag, p ->
Pair(userAndFlag, p)
}
.take(1)
.filter {
it.second.isStarred() != it.first.first
}.switchMap {
if (it.first.first) {
this.saveProject(it.second)
} else {
this.unSaveProject(it.second)
}
}.share()

val currentProject = Observable.merge(
initialProject,
refreshedProjectNotification.compose(values()),
projectOnUserChangeSave,
savedProjectOnLoginSuccess
savedProjectOnLoginSuccess,
projectOnDeepLinkChangeSave
)

val projectSavedStatus = projectOnUserChangeSave.mergeWith(savedProjectOnLoginSuccess)
val projectSavedStatus = Observable.merge(projectOnUserChangeSave, savedProjectOnLoginSuccess, projectOnDeepLinkChangeSave)

projectSavedStatus
.compose(bindToLifecycle())
Expand Down

0 comments on commit b90e63d

Please sign in to comment.