MBL-2723: Add deeplink support for pledge manager edit order (#2427)

This commit is contained in:
Yun Cheng
2025-11-06 16:42:43 -05:00
committed by GitHub
parent f7a12e2bd4
commit 64b328df89
7 changed files with 178 additions and 1 deletions

View File

@@ -84,7 +84,8 @@ enum class FlagKey(val key: String) {
ANDROID_COMPLETED_PM_CHECKOUT_WEBVIEW("android_completed_pm_checkout_webview"),
ANDROID_NET_NEW_BACKER_GO_TO_PM_WEBVIEW("android_net_new_backer_go_to_pm_webview"),
ANDROID_PLOT_EDIT_PLEDGE("android_plot_edit_pledge"),
ANDROID_NATIVE_ONBOARDING_FLOW("android_native_onboarding_flow")
ANDROID_NATIVE_ONBOARDING_FLOW("android_native_onboarding_flow"),
ANDROID_EDIT_ORDER("android_edit_order")
}
fun FeatureFlagClient.getFetchInterval(): Long =

View File

@@ -211,6 +211,18 @@ fun Uri.isBackingDetailsUri(webEndpoint: String): Boolean {
return isKickstarterUri(webEndpoint) && PROJECT_BACKING_DETAILS_URL.matcher(path()).matches()
}
/**
* Returns `true` if the URI is a post-campaign checkout URL for editing an order and the corresponding feature flag is enabled,
* `false` otherwise.
*
* This will match URIs with the format: `/projects/creator/project/order_edits/order_edit_id/checkout`
*/
fun Uri.isPMOrderEditUri(webEndpoint: String, isFFEnabled: Boolean = false): Boolean {
return isKickstarterUri(webEndpoint) &&
PM_ORDER_EDIT_CHECKOUT_PATTERN.matcher(path()).matches() &&
isFFEnabled
}
fun Uri.hasSecretRewardToken(): Boolean {
return this.getQueryParameter("secret_reward_token")?.isNotEmpty() == true
}
@@ -330,3 +342,8 @@ private val MAIN_PAGE_OPEN_BUTTON_QUERYPARAMS = Pattern.compile(
private val PROJECT_BACKING_DETAILS_URL = Pattern.compile(
"\\A\\/projects(\\/[a-zA-Z0-9_-]+)?\\/[a-zA-Z0-9_-]+\\/backing\\/details\\z"
)
// /projects/:creator_param/:project_param/order_edits/:order_edit_id/checkout
private val PM_ORDER_EDIT_CHECKOUT_PATTERN = Pattern.compile(
"\\A\\/projects(\\/[a-zA-Z0-9_-]+)?\\/[a-zA-Z0-9_-]+\\/order_edits\\/[a-zA-Z0-9_-]+\\/checkout\\z"
)

View File

@@ -32,6 +32,7 @@ object IntentKey {
const val SURVEY_RESPONSE = "com.kickstarter.kickstarter.survey_response"
const val NOTIFICATION_SURVEY_RESPONSE = "com.kickstarter.kickstarter.survey_response"
const val DEEPLINK_SURVEY_RESPONSE = "com.kickstarter.kickstarter.deeplink_survey_response"
const val DEEPLINK_PM_ORDER_EDIT = "com.kickstarter.kickstarter.deeplink_pm_order_edit"
const val NOTIFICATION_PLEDGE_REDEMPTION = "com.kickstarter.kickstarter.notification_pledge_redeption"
const val TOOLBAR_TITLE = "com.kickstarter.kickstarter.intent_toolbar_title"
const val TRACKING_CLIENT_TYPE_TAG = "com.kickstarter.kickstarter.intent_tracking_client_tag"

View File

@@ -27,6 +27,7 @@ import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.data.LoginReason
import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck
import com.kickstarter.ui.extensions.startPreLaunchProjectActivity
import com.kickstarter.ui.extensions.startWebViewActivity
import com.kickstarter.viewmodels.DeepLinkViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
@@ -137,6 +138,19 @@ class DeepLinkActivity : AppCompatActivity() {
}
}.addToDisposable(disposables)
viewModel.outputs.startPMOrderEditWebview()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val uri = it.first
val isLoggedIn = it.second
if (isLoggedIn) {
startPMOrderEditActivity(uri.toString())
} else {
startLoginForPMOrderEdit(uri.toString())
}
}.addToDisposable(disposables)
statsigClient.updateExperimentUser()
}
@@ -237,6 +251,19 @@ class DeepLinkActivity : AppCompatActivity() {
finish()
}
private fun startLoginForPMOrderEdit(url: String) {
val intent = Intent(this, LoginToutActivity::class.java)
.putExtra(IntentKey.LOGIN_REASON, LoginReason.DEFAULT)
.putExtra(IntentKey.DEEPLINK_PM_ORDER_EDIT, url)
startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW)
}
private fun startPMOrderEditActivity(url: String) {
ApplicationUtils.startNewDiscoveryActivity(this)
startWebViewActivity(url, getString(R.string.fpo_review_edits))
finish()
}
override fun onDestroy() {
disposables.clear()
super.onDestroy()

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider
import com.kickstarter.libs.CurrentUserTypeV2
import com.kickstarter.libs.Environment
import com.kickstarter.libs.RefTag
import com.kickstarter.libs.featureflag.FlagKey
import com.kickstarter.libs.rx.transformers.Transformers
import com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair
import com.kickstarter.libs.utils.UrlUtils.appendRefTag
@@ -21,6 +22,7 @@ import com.kickstarter.libs.utils.extensions.isKSDomain
import com.kickstarter.libs.utils.extensions.isMainPage
import com.kickstarter.libs.utils.extensions.isNotNull
import com.kickstarter.libs.utils.extensions.isNull
import com.kickstarter.libs.utils.extensions.isPMOrderEditUri
import com.kickstarter.libs.utils.extensions.isProjectCommentUri
import com.kickstarter.libs.utils.extensions.isProjectPreviewUri
import com.kickstarter.libs.utils.extensions.isProjectSaveUri
@@ -30,6 +32,7 @@ import com.kickstarter.libs.utils.extensions.isProjectUpdateUri
import com.kickstarter.libs.utils.extensions.isProjectUri
import com.kickstarter.libs.utils.extensions.isRewardFulfilledDl
import com.kickstarter.libs.utils.extensions.isSettingsUrl
import com.kickstarter.libs.utils.extensions.isTrue
import com.kickstarter.models.Project
import com.kickstarter.models.User
import com.kickstarter.services.ApiClientTypeV2
@@ -77,6 +80,8 @@ interface DeepLinkViewModel {
fun startProjectSurvey(): Observable<Pair<Uri, Boolean>>
fun startPMOrderEditWebview(): Observable<Pair<Uri, Boolean>>
/** Emits a Project and RefTag pair when we should start the [com.kickstarter.ui.activities.PreLaunchProjectPageActivity]. */
fun startPreLaunchProjectActivity(): Observable<Pair<Uri, Project>>
}
@@ -93,6 +98,8 @@ interface DeepLinkViewModel {
private val startProjectActivityWithCheckout = BehaviorSubject.create<Uri>()
private val startProjectActivityToSave = BehaviorSubject.create<Uri>()
private val startProjectSurvey = BehaviorSubject.create<Pair<Uri, Boolean>>()
private val startPMOrderEditWebview = BehaviorSubject.create<Pair<Uri, Boolean>>()
private val updateUserPreferences = BehaviorSubject.create<Boolean>()
private val finishDeeplinkActivity = BehaviorSubject.create<Unit>()
private val apolloClient = requireNotNull(environment.apolloClientV2())
@@ -262,6 +269,17 @@ interface DeepLinkViewModel {
startProjectSurvey.onNext(it)
}.addToDisposable(disposables)
uriFromIntent
.filter { it.isPMOrderEditUri(webEndpoint, ffClient.getBoolean(FlagKey.ANDROID_EDIT_ORDER)) }
.map { appendRefTagIfNone(it) }
.withLatestFrom(this.currentUser.isLoggedIn) { url, isLoggedIn ->
return@withLatestFrom Pair(url, isLoggedIn)
}
.filter { it.second.isTrue() }
.subscribe {
startPMOrderEditWebview.onNext(it)
}.addToDisposable(disposables)
currentUser.observable()
.filter { it.isPresent() }
.map { it.getValue() }
@@ -316,6 +334,12 @@ interface DeepLinkViewModel {
.filter { !it.isRewardFulfilledDl() }
.filter { !it.isEmailDomain() }
.filter { !it.isProjectSurveyUri(webEndpoint) }
.filter {
!it.isPMOrderEditUri(
webEndpoint,
ffClient.getBoolean(FlagKey.ANDROID_EDIT_ORDER)
)
}
Observable.merge(projectPreview, unsupportedDeepLink)
.map { obj: Uri -> obj.toString() }
@@ -395,6 +419,8 @@ interface DeepLinkViewModel {
override fun startProjectSurvey(): Observable<Pair<Uri, Boolean>> = startProjectSurvey
override fun startPMOrderEditWebview(): Observable<Pair<Uri, Boolean>> = startPMOrderEditWebview
override fun startPreLaunchProjectActivity(): Observable<Pair<Uri, Project>> = startPreLaunchProjectActivity
}

View File

@@ -104,4 +104,6 @@
<!-- FPO's for PPO v2 -->
<string name="fpo_the_creator_has_not_collected_your_address_please_contact_them_to_resolve_the_issue" formatted="false">The creator has not collected your address. Please contact them to resolve the issue.</string>
<!-- FPO's for order edit -->
<string name="fpo_review_edits" formatted="false">Review edits made to your order</string>
</resources>

View File

@@ -39,6 +39,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
private val startProjectActivityForCommentToUpdate = TestSubscriber<Uri>()
private val startPreLaunchProjectActivity = TestSubscriber<Pair<Uri, Project>>()
private val startProjectSurveyActivity = TestSubscriber<Pair<Uri, Boolean>>()
private val startPMOrderEditWebview = TestSubscriber<Pair<Uri, Boolean>>()
private val finishDeeplinkActivity = TestSubscriber<Unit>()
private val disposables = CompositeDisposable()
@@ -64,6 +65,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
vm.outputs.finishDeeplinkActivity().subscribe { finishDeeplinkActivity.onNext(it) }.addToDisposable(disposables)
vm.outputs.startPreLaunchProjectActivity().subscribe { startPreLaunchProjectActivity.onNext(it) }.addToDisposable(disposables)
vm.outputs.startProjectSurvey().subscribe { startProjectSurveyActivity.onNext(it) }.addToDisposable(disposables)
vm.outputs.startPMOrderEditWebview().subscribe { startPMOrderEditWebview.onNext(it) }.addToDisposable(disposables)
}
@After
@@ -86,6 +88,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -105,6 +108,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startPreLaunchProjectActivity.assertNoValues()
finishDeeplinkActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -138,6 +142,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -172,6 +177,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -187,6 +193,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -202,6 +209,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -217,6 +225,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -234,6 +243,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -250,6 +260,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -267,6 +278,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -283,6 +295,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -301,6 +314,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -318,6 +332,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -333,6 +348,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertValueCount(1)
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -348,6 +364,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertValueCount(1)
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -363,6 +380,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertValueCount(1)
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -378,6 +396,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertValueCount(1)
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -393,6 +412,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertValueCount(1)
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -418,6 +438,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -442,6 +463,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -456,6 +478,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -473,6 +496,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -490,6 +514,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -505,6 +530,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -532,6 +558,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -559,6 +586,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -584,6 +612,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -601,6 +630,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
@@ -624,6 +654,7 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityForCheckout.assertNoValues()
startProjectActivityForComment.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
startBrowser.assertNoValues()
}
@@ -657,10 +688,82 @@ class DeepLinkViewModelTest : KSRobolectricTestCase() {
startProjectActivityForComment.assertNoValues()
startBrowser.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
finishDeeplinkActivity.assertValueCount(1)
startPreLaunchProjectActivity.assertValueCount(1)
}
@Test
fun testPMOrderEditDeeplink_startsPMOrderEditWebview_featureFlagDisabled() {
val url = "https://www.kickstarter.com/projects/1768690592/reclaimed-coffee-video-game/order_edits/5/checkout"
setUpEnvironment(intent = intentWithData(url))
startBrowser.assertValueCount(1)
startDiscoveryActivity.assertNoValues()
startProjectActivity.assertNoValues()
startProjectActivityForCheckout.assertNoValues()
startProjectActivityForComment.assertNoValues()
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
@Test
fun testPMOrderEditDeeplink_startsPMOrderEditWebview_featureFlagEnabled_UserLoggedIn() {
val url = "https://www.kickstarter.com/projects/1768690592/reclaimed-coffee-video-game/order_edits/5/checkout"
val mockFeatureFlagClient: MockFeatureFlagClient =
object : MockFeatureFlagClient() {
override fun getBoolean(FlagKey: FlagKey): Boolean {
return true
}
}
val env = environment()
.toBuilder()
.currentUserV2(MockCurrentUserV2(UserFactory.user()))
.featureFlagClient(mockFeatureFlagClient)
.build()
setUpEnvironment(intent = intentWithData(url), environment = env)
startBrowser.assertNoValues()
startDiscoveryActivity.assertNoValues()
startProjectActivity.assertNoValues()
startProjectActivityForCheckout.assertNoValues()
startProjectActivityForComment.assertNoValues()
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertValueCount(1)
}
@Test
fun testPMOrderEditDeeplink_startsPMOrderEditWebview_featureFlagEnabled_UserLoggedOut() {
val url = "https://www.kickstarter.com/projects/1768690592/reclaimed-coffee-video-game/order_edits/5/checkout"
val mockFeatureFlagClient: MockFeatureFlagClient =
object : MockFeatureFlagClient() {
override fun getBoolean(FlagKey: FlagKey): Boolean {
return true
}
}
val env = environment()
.toBuilder()
.currentUserV2(MockCurrentUserV2()) // using empty constructor means no user logged in
.featureFlagClient(mockFeatureFlagClient)
.build()
setUpEnvironment(intent = intentWithData(url), environment = env)
startBrowser.assertNoValues()
startDiscoveryActivity.assertNoValues()
startProjectActivity.assertNoValues()
startProjectActivityForCheckout.assertNoValues()
startProjectActivityForComment.assertNoValues()
startProjectActivityToSave.assertNoValues()
startPreLaunchProjectActivity.assertNoValues()
startProjectSurveyActivity.assertNoValues()
startPMOrderEditWebview.assertNoValues()
}
private fun mockApiSetBacking(backing: Backing, completed: Boolean): MockApiClientV2 {
return object : MockApiClientV2() {
override fun postBacking(