When a user signs in to your application, you need to provide them with a choice to opt into the IQL Test Drive program. Once the users opt in, you can present them with exciting auto insurance offers, customizing your Ads based on user performance. It's a great opportunity for you to entice the user to sign up by offering discounts for good driving behavior.
Your users can use the application to monitor their own driving performance and improve their driving behavior to earn better discounts. IQL keeps users motivated to bring down their insurance burden while emphasizing safe driving.
When a user interacts with the IQL application or program, you need to be able to distinguish between new and returning users. Returning users fall into different categories, such as:
Users who have switched to a new phone.
Users who have uninstalled and reinstalled the IQL Ref application.
Users who have opted out of the program and want to rejoin.
To provide a seamless experience, we recommend using a slightly different messaging for returning users. Additionally, you can improve their user experience by pre-filling forms or skipping screens entirely, as most of the necessary data will already be available to you. By implementing these strategies, you can differentiate between new and returning users and provide a more tailored, user-friendly experience.
Here's an example of how a user opts into the IQL program:
Opt-in Flow for New Users
Retrieving and Storing Existing User Data
To enhance user experience, it is important to retrieve and store any existing user data in the application. Use the driverInfo Endpoint to retrieve existing data and save it within the application.
Additionally, maintaining different states for the user can also help provide a better user experience.
As the user state will be unknown at the start, we recommend that you call the driverInfo Endpointto retrieve any existing user details and to save them locally within the application. By doing so, you can avoid calling the same API again, which helps improve application performance.
classAutoFragment : Fragment(R.layout.fragment_auto) {private lateinit var binding:FragmentAutoBindingprivate val viewModel: OnboardingViewModel by activityViewModels()private val permissionViewModel: PermissionsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)AppUpgrade.instance.checkForUpdate()viewModel.loadUserState() …… …… …… } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding =FragmentAutoBinding.bind(view)checkNetworkConnectivity()observeUserState() …… …… …… }private fun observeUserState() {viewLifecycleOwner.lifecycleScope.launch {viewModel.userState.collectLatest { userState ->Timber.d("User State in Auto Fragment is $userState") userState?.let {if (it ==UserState.UNKNOWN) {viewModel.executeDriverInfoApi() } } } }}}classOnboardingViewModel : ViewModel() { ….. ….. …..private val _userState =MutableStateFlow<UserState?>(null) val userState:StateFlow<UserState?>=_userState.asStateFlow()private val userStatePreference =ZWLCore.userStatePreference() ….. ….. fun loadUserState() {_userState.value=userStatePreference.getUserState() }fun executeDriverInfoApi() {viewModelScope.launch(AppDispatchers.IO) { val driverId =getDriverId()Timber.d("Driver Id in executeDriverInfoApi is $driverId")if (driverId.isNullOrEmpty()) {return@launch }if (userStatePreference.getUserState()?.equals(UserState.UNKNOWN) ==true) {ApiUtils.resetDriverInfoApiTimeStamp() }ApiUtils.executeApiUsingTimeStamps( previousApiTimeStamp =ApiUtils.driverInfoApiTimeStamp, onExecuteApi = { calender ->ApiUtils.executeApi( calendar = calender, onTimeChanged = { timeStamp ->ApiUtils.driverInfoApiTimeStamp= timeStamp }, onExecute = { val success =ZWLCore.driverInfoApi().invoke(driverId)if (success.not()) {ApiUtils.resetDriverInfoApiTimeStamp() } } ) } ) }}fun getDriverId(): String? {returnZWLCore.loginRepository().getDriverIdFromPreferences()}}
private const val USER_STATE = "user_state"
class UserStatePreference(
private val sharedPreferences: SharedPreferences
) {
fun setUserState(userState: UserState) {
sharedPreferences.edit().putString(USER_STATE, userState.asJson()).apply()
}
fun getUserState(): UserState? {
val defaultValue = UserState.UNKNOWN.asJson()
return sharedPreferences.getString(USER_STATE, defaultValue)?.toUserState()
}
}
object ApiUtils {
....
const val DEFAULT_TIME_IN_MILLI_SECONDS = 0L
....
var driverInfoApiTimeStamp = 0L
....
inline fun executeApiUsingTimeStamps(
previousApiTimeStamp: Long,
onExecuteApi: (Calendar) -> Unit
) {
Timber.d("Previous Time Stamp $previousApiTimeStamp")
val calendar = Calendar.getInstance()
val currentTime = calendar.time.time
if (previousApiTimeStamp != DEFAULT_TIME_IN_MILLI_SECONDS) {
if (currentTime > previousApiTimeStamp) {
onExecuteApi(calendar)
} else {
Timber.d("Need to hit Api after $previousApiTimeStamp")
}
} else {
onExecuteApi(calendar)
}
}
fun resetDriverInfoApiTimeStamp() {
driverInfoApiTimeStamp = DEFAULT_TIME_IN_MILLI_SECONDS
}
....
}
class DriverInfoApi(
private val api: Api,
private val userStatePreference: UserStatePreference,
private val personalInformationRepository: PersonalInformationRepository
) {
suspend operator fun invoke(driverId: String): Boolean {
when (val response = api.getDriverInfo(driverId)) {
is ApiResponse.Success -> {
val driverInfo = response.data
Timber.d("DriverInfoApi success: $driverInfo")
handleSuccessResponse(
driverInfo = driverInfo
)
return true
}
is ApiResponse.Failure -> {
Timber.d("DriverInfoApi failed: ${response.failure.message}")
Timber.d("UserState saved as UserState.UNKNOWN")
userStatePreference.setUserState(UserState.UNKNOWN)
return false
}
is ApiResponse.Error -> {
Timber.d("DriverInfoApi error: ${response.throwable.localizedMessage}")
Timber.d("UserState saved as UserState.UNKNOWN")
userStatePreference.setUserState(UserState.UNKNOWN)
return false
}
}
}
private suspend fun handleSuccessResponse(driverInfo: DriverInfo) {
if (isReturningUser(driverInfo).not()) {
userStatePreference.setUserState(UserState.NEW)
Timber.d("UserState saved as UserState.NEW")
return
}
userStatePreference.setUserState(UserState.RETURNING)
Timber.d("UserState saved as UserState.RETURNING")
savePersonalInformation(
driverInfo = driverInfo
)
}
private suspend fun savePersonalInformation(driverInfo: DriverInfo) {
val personalInformation =
personalInformationRepository.getPersonalInformation() ?: return
val newPersonalInformation = personalInformation.copy(
address = Address(
countryCode = driverInfo.country,
stateCode = driverInfo.state,
zipCode = driverInfo.zipcode
),
insurer = driverInfo.insurer
)
personalInformationRepository.savePersonalInformation(newPersonalInformation)
}
private fun isReturningUser(driverInfo: DriverInfo): Boolean {
return driverInfo.country.isNullOrEmpty().not() &&
driverInfo.insurer.isNullOrEmpty().not() &&
driverInfo.zipcode.isNullOrEmpty().not() &&
driverInfo.state.isNullOrEmpty().not()
}
}
Retrieving Driving Insights for Returning Users
By retrieving a returning user's driving insights, you can create a more personalized experience. Use the driverStatus Endpointto retrieve the user's existing driving insights and retain it locally within the application for easy access.
You need to also provide a user reactivation experience which can involve displaying driving insights along with personalized recommendations.
On the other hand, if it is determined that the user is a new user, it is important to provide a new user experience. This could involve displaying an introduction to the program and its features, and providing guidance on how to get started.
The following code snippets demonstrate how to retrieve and persist driving insights data for returning users:
class ExplainerFragment : Fragment(R.layout.fragment_explainer) {
private lateinit var binding: FragmentExplainerBinding
private val viewModel: OnboardingViewModel by activityViewModels()
private val appName: String by lazy {
getString(R.string.app_name)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentExplainerBinding.bind(view)
checkForReturningUser()
viewModel.loadUserState()
observerDriverStatus()
…..
…..
}
private fun checkForReturningUser() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.userState.collectLatest { userState ->
Timber.d("User State in Explainer Fragment is $userState")
userState?.let {
when (it) {
UserState.RETURNING -> {
viewModel.executeDriverStatusApi()
}
UserState.UNKNOWN -> {
binding.explainerLoader.makeVisible()
binding.explainerMainLayout.makeGone()
viewModel.executeDriverInfoApi()
viewModel.executeDriverStatusApi()
}
UserState.NEW -> {
Timber.d("This is a new User")
binding.explainerLoader.makeGone()
binding.explainerMainLayout.makeVisible()
}
}
}
}
}
}
private fun observerDriverStatus() {
lifecycleScope.launch {
viewModel.driverStatus
.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
.distinctUntilChanged()
.collectLatest {
setUpView(it)
}
}
}
private fun setUpView(driverStatus: IQLDriverStatus?) {
if (driverStatus?.metrics != null && driverStatus.metrics!!.dateFirstDrive != null) {
setProgressIndicatorCard(driverStatus)
} else {
setHowItWorksCard()
}
binding.explainerLoader.makeGone()
binding.explainerMainLayout.makeVisible()
}
}
class OnboardingViewModel : ViewModel() {
…..
…..
…..
private val _driverInfo = MutableStateFlow<DriverInfo?>(null)
val driverInfo: StateFlow<DriverInfo?> = _driverInfo.asStateFlow()
…..
…..
…..
fun loadUserState() {
_userState.value = userStatePreference.getUserState()
}
Here is the overall design implementation for the IQL Reference Application:
3.0. Publisher Integration Testing Checklist
Use the following testing checklist to test the IQL test drive program introduction to users:
Opt-in Process: Ensure that the program opt-in section is prominently displayed and easy to locate. The section should allow the user to enter into the program by providing their consent.
How the Program Works: Ensure that there is a clear and concise explanation of how the program works and how the user will benefit from it. The explanation should be easily accessible and understandable for all users.
FAQ: Ensure that the FAQ section (if available) is easily accessible and contains accurate and helpful information for all users.
Activation: Ensure that the user is able to proceed further to the capture user information flow without any issues or errors.
Upon Cancelation: If the user cancels the activation, ensure that a confirmation prompt is displayed to remind them about the value that they might miss and give them an opportunity to reconsider.