Implementing Stripe payment in Eventyay Attendee

In Eventyay Attendee, getting tickets for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Stripe?
  • Implementing Stripe Payment in Eventyay Attendee
  • Conclusion
  • Resources

WHY USING STRIPE?

There are many great APIs to be taken into consideration for making payments but we choose Stripe as one of our payment gateways because of simple implementations, detailed documentation, a good number of supported card type and good security support

IMPLEMENTING STRIPE PAYMENT IN EVENTYAY ATTENDEE

Step 1: Setup dependency in the build.gradle

// Stripe
implementation ‘com.stripe:stripe-android:10.3.0’

Step 2: Set up UI to take card information

The information needed for making payments are Card Number, CVC, Expiration Date, which can be made with simple UI (EditText, Spinner,…). Stripe support getting information with CardInputWidget but we made a custom UI for that. Here is the UI we created.

Step 3: Create a card and validate information

Stripe has an object called Card, which takes card number, expiration date and CVC number as parameter to detect the card type and validate the card information with function .validateCard()

PAYMENT_MODE_STRIPE -> {
  card = Card.create(rootView.cardNumber.text.toString(), attendeeViewModel.monthSelectedPosition,
      rootView.year.selectedItem.toString().toInt(), rootView.cvc.text.toString())

  if (!card.validateCard()) {
      rootView.snackbar(getString(R.string.invalid_card_data_message))
      false
  } else {
      true
  }
}

Step 4: Send the token to the server

If card information is valid, we can create a token from the Card and then send it to the server. The token will act as the identifier of the card in order for the server to charge the payment and create tickets for the user. 

private fun sendToken(card: Card) {
  Stripe(requireContext())
      .createToken(card, BuildConfig.STRIPE_API_KEY, object : TokenCallback {
          override fun onSuccess(token: Token) {
              val charge = Charge(attendeeViewModel.getId().toInt(), token.id, null)
              attendeeViewModel.chargeOrder(charge)
          }
          override fun onError(error: Exception) {
              rootView.snackbar(error.localizedMessage.toString())
          }
      })
}

Step 5: So the rest is already handled by the server. Android application will then just receive the response from the server to see if the order is charged successfully or not.

CONCLUSION

With Stripe, user can easily make payments to get tickets for events. Stripe is a great payment gateway as it is really easy to implement in Android. Hopefully, this blog post will help you create a great shopping cart app or any kind of application that requires fast, simple and easy payments.

RESOURCES

Eventyay Attendee Pull Request on Stripe: https://github.com/fossasia/open-event-attendee-android/pull/1863

Documentation from Stripe for Android: https://stripe.com/docs/mobile/android


Continue Reading

Adding time counter on ordering tickets in Eventyay Attendee

In Eventyay Attendee, ordering tickets for events has always been a core functionality that we focus on. When ordering tickets, adding a time counter to make a reservation and release tickets after timeout is a common way to help organizers control their tickets’ distribution and help users save up their tickets. Let’s take a look at how to implement this feature

  • Implementing the time counter 
  • Some notes on implementing time counter
  • Conclusion
  • Resources

INTEGRATING TIME COUNTER TO YOUR SYSTEM

Step 1: Create the UI for your time counter. In here, we made a simple View container with TextView inside to update the time.

Step 2: Set up the time counter with Android CountdownTimer with the total countdown time and the ticking time. In Eventyay, the default countdown time is 10 minutes (600,000 ms) with the ticking time is (1,000 ms), which means the UI is updated every one second.

private fun setupCountDownTimer(orderExpiryTime: Int) {
  rootView.timeoutCounterLayout.isVisible = true
  rootView.timeoutInfoTextView.text =
      getString(R.string.ticket_timeout_info_message, orderExpiryTime.toString())

  val timeLeft: Long = if (attendeeViewModel.timeout == -1L) orderExpiryTime * 60 * 1000L
                          else attendeeViewModel.timeout
  timer = object : CountDownTimer(timeLeft, 1000) {
      override fun onFinish() {
          findNavController(rootView).navigate(AttendeeFragmentDirections
              .actionAttendeeToTicketPop(safeArgs.eventId, safeArgs.currency, true))
      }

      override fun onTick(millisUntilFinished: Long) {
          attendeeViewModel.timeout = millisUntilFinished
          val minutes = millisUntilFinished / 1000 / 60
          val seconds = millisUntilFinished / 1000 % 60
          rootView.timeoutTextView.text = “$minutes:$seconds”
      }
  }
  timer.start()
}

Step 3: Set up creating a pending order when the timer starts counting so that users can hold a reservation for their tickets. A simple POST request about empty order to the API is made

fun initializeOrder(eventId: Long) {
  val emptyOrder = Order(id = getId(), status = ORDER_STATUS_INITIALIZING, event = EventId(eventId))

  compositeDisposable += orderService.placeOrder(emptyOrder)
      .withDefaultSchedulers()
      .subscribe({
          mutablePendingOrder.value = it
          orderIdentifier = it.identifier.toString()
      }, {
          Timber.e(it, “Fail on creating pending order”)
      })
}

Step 4: Set up canceling order when the time counter finishes. As time goes down, the user should be redirected to the previous fragment and a pop-up dialog should show with a message about reservation time has finished. There is no need to send an HTTP request to cancel the pending order as it is automatically handled by the server.

Step 5: Cancel the time counter in case the user leaves the app unexpectedly or move to another fragment. If this step is not made, the CountdownTimer still keeps counting in the background and possibly call onFinished() at some point that could evoke functions and crash the app

override fun onDestroy() {
  super.onDestroy()
  if (this::timer.isInitialized)
      timer.cancel()
}

RESULTS

CONCLUSION

For a project with a ticketing system, adding a time counter for ordering is a really helpful feature to have. With the help of Android CountdownTimer, it is really to implement this function to enhance your user experience.

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1843 – Add time counter on ordering ticket

Documentation: https://developer.android.com/reference/android/os/CountDownTimer

Continue Reading

Tax Information on Public Ticket Page

This blog post will elaborate on how Tax Information is being displayed on the public page of an event. In current implementation, the user gets to know the total tax inclusive amount only after he/she decides to place an order but no such information was given to them on the public ticket page itself.

Order summary example in eventyay

Example : In initial implementation, the user gets to know that the order is of only $120 and no information is given about the additional 30% being charged and taking the total to $156.

To tackle this issue, I added two hybrid components to the ticket object to handle the two tax cases : 

  • Inclusion in the price : In European and Asian Countries , the tax amount is included in the ticket price itself. For this case, I created the following parameter to store the tax amount included in gross amount.
// app/models/ticket.js
includedTaxAmount: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  const taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (taxType) {
    const taxRate = this.event.get('tax.rate');
    return ((taxRate * this.price) / (100 + taxRate)).toFixed(2);
  }
  return 0;
})
  • Added on the ticket price : In basic US tax policy, the tax amount is added on top of the ticket price. For such cases I have added a new attribute to ticket model which calculates the total amount payable for that particular ticket with tax inclusion
// app/models/ticket.js
ticketPriceWithTax: computed('event.tax.isTaxIncludedInPrice', 'event.tax.rate', function() {
  let taxType = this.event.get('tax.isTaxIncludedInPrice');
  if (!taxType) {
    return ((1 + this.event.get('tax.rate') / 100) * this.price).toFixed(2);
  }
  return this.price;
})

Now, the public ticket page has to be edited accordingly. The design I decided to follow is inspired by eventbrite itself : 

Eventbrite specimen of the proposed implementation

For this implementation, I modified the ticket list template to accommodate the changes in the following way : 

// app/components/public/ticket-list.hbs
<
td id="{{ticket.id}}_price">
{{currency-symbol eventCurrency}} {{format-number ticket.price}}
{{#
if (and taxInfo (not-eq ticket.type 'free'))}}
  {{#
if showTaxIncludedMessage}}
    <
small class="ui gray-text small">
      {{t 'includes'}} {{currency-symbol eventCurrency}} {{format-number ticket.includedTaxAmount}}
    </
small>
  {{else}}
    <
small class="ui gray-text small">
      + {{currency-symbol eventCurrency}} {{format-number (sub ticket.ticketPriceWithTax ticket.price)}}
    </
small>
  {{/
if}}
  <
div>
    <
small class="ui gray-text tiny aligned right">({{taxInfo.name}})</small>
  </
div>
{{/
if}}
</
td>
Tax amount is included in ticket price

Hence making the new public ticket list display to look like this in case of tax amount inclusion and additional charge as follows

Tax amount is charged over the base price

Discount Code application cases:

In the cases when a user applies the discount code, the ticket price need to be updated, hence, the tax applied has to be updated accordingly. I achieved this by updating the two computed properties of the ticket model on each togglePromotionalCode and applyPromotionalCode action. When a promotional code is applied, the appropriate attribute is updated according to the discount offered.

// app/components/public/ticket-list.js
tickets.forEach(ticket => {
let ticketPrice = ticket.get('price');
let taxRate = ticket.get('event.tax.rate');
let discount = discountType === 'amount' ? Math.min(ticketPrice, discountValue) : ticketPrice * (discountValue / 100);
ticket.set('discount', discount);
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = (ticketPrice - ticket.discount) * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * (ticketPrice - discount)) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
this.discountedTickets.addObject(ticket);

Similarly, on toggling the discount code off, the ticket’s computed properties are set back to their initial value using the same formula kept during the time of initialization which has been achieved in the following manner.

// app/components/public/ticket-list.js
this.discountedTickets.forEach(ticket => {
let taxRate = ticket.get('event.tax.rate');
let ticketPrice = ticket.get('price');
if (taxRate && !this.showTaxIncludedMessage) {
  let ticketPriceWithTax = ticketPrice * (1 + taxRate / 100);
  ticket.set('ticketPriceWithTax', ticketPriceWithTax);
} else if (taxRate && this.showTaxIncludedMessage) {
  let includedTaxAmount = (taxRate * ticketPrice) / (100 + taxRate);
  ticket.set('includedTaxAmount', includedTaxAmount);
}
ticket.set('discount', 0);
});

This particular change makes sure that the tax amount is calculated properly as per the discounted amount and thus eliminates the possibility of overcharging the attendee.

Tax recalculation for discounted tickets

In conclusion, this feature has been implemented keeping in mind the consumer’s interest in using the Open Event Frontend and the ease of tax application on the public level with minimum required network requests.

Resources

Related Work and Code Repository

Continue Reading

Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

In Eventyay Attendee, searching for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Mapbox?
  • Integrating places autosuggestion for searching
  • Conclusion
  • Resources

WHY USING MAPBOX?

There are many Map APIs to be taken into consideration but we choose Mapbox as it is really to set up and use, good documentation and reasonable pricing for an open-source project compared to other Map API.

INTEGRATING PLACES AUTOSUGGESTION FOR SEARCHING

Step 1: Setup dependency in the build.gradle + the MAPBOX key

//Mapbox java sdk
implementation ‘com.mapbox.mapboxsdk:mapbox-sdk-services:4.8.0’

Step 2: Set up functions inside ViewModel to handle autosuggestion based on user input:

private fun loadPlaceSuggestions(query: String) {
  // Cancel Previous Call
  geoCodingRequest?.cancelCall()
  doAsync {
      geoCodingRequest = makeGeocodingRequest(query)
      val list = geoCodingRequest?.executeCall()?.body()?.features()
      uiThread { placeSuggestions.value = list }
  }
}

private fun makeGeocodingRequest(query: String) = MapboxGeocoding.builder()
  .accessToken(BuildConfig.MAPBOX_KEY)
  .query(query)
  .languages(“en”)
  .build()

Based on the input, the functions will update the UI with new inputs of auto-suggested location texts. The MAPBOX_KEY can be given from the Mapbox API.

Step 3: Create an XML file to display autosuggestion strings item and set up RecyclerView in the main UI fragment

Step 4: Set up ListAdapter and ViewHolder to bind the list of auto-suggested location strings. Here, we use CamenFeature to set up with ListAdapter as the main object. With the function .placeName(), information about the location will be given so that ViewHolder can bind the data

class PlaceSuggestionsAdapter :
  ListAdapter<CarmenFeature,
      PlaceSuggestionViewHolder>(PlaceDiffCallback()) {

  var onSuggestionClick: ((String) -> Unit)? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceSuggestionViewHolder {
      val itemView = LayoutInflater.from(parent.context)
          .inflate(R.layout.item_place_suggestion, parent, false)
      return PlaceSuggestionViewHolder(itemView)
  }

  override fun onBindViewHolder(holder: PlaceSuggestionViewHolder, position: Int) {
      holder.apply {
          bind(getItem(position))
          onSuggestionClick = [email protected]
      }
  }

  class PlaceDiffCallback : DiffUtil.ItemCallback<CarmenFeature>() {
      override fun areItemsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.placeName() == newItem.placeName()
      }

      override fun areContentsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.equals(newItem)
      }
  }
}
fun bind(carmenFeature: CarmenFeature) {
  carmenFeature.placeName()?.let {
      val placeDetails = extractPlaceDetails(it)
      itemView.placeName.text = placeDetails.first
      itemView.subPlaceName.text = placeDetails.second
      itemView.subPlaceName.isVisible = placeDetails.second.isNotEmpty()

      itemView.setOnClickListener {
          onSuggestionClick?.invoke(placeDetails.first)
      }
  }
}

Step 5: Set up RecyclerView with Adapter created above:

private fun setupRecyclerPlaceSuggestions() {
  rootView.rvAutoPlaces.layoutManager = LinearLayoutManager(context)
  rootView.rvAutoPlaces.adapter = placeSuggestionsAdapter

  placeSuggestionsAdapter.onSuggestionClick = {
      savePlaceAndRedirectToMain(it)
  }
}

RESULTS

CONCLUSION

Place Autocorrection is a really helpful and interesting feature to include in your next project. With the help of Mapbox SDK, it is really easy to implement to enhance your user experience in your application.

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee PR: #1594 – feat: Mapbox Autosuggest

Documentation: https://docs.mapbox.com/android/plugins/overview/places/

Continue Reading

Implementing Attendee Forms in Wizard of Open Event Frontend

This blog post illustrates on how the order form is included in the attendee information of the Open Event Frontend form  and enabling the organizer to choosing what information to collect from the attendee apart from the mandatory data i.e. First Name, Last Name and the Email Id during the creation of event itself.

The addition of this feature required alteration in the existing wizard flow to accommodate this extra step. This new wizard flow contains the step :

  • Basic Details : Where organizer fills the basic details regarding the event.
  • Attendee Form : In this step, the organizer can choose what information he/she has to collect from the ticket buyers.
  • Sponsors : This step enables the organizer to fill in the sponsor details
  • Session and Speakers : As the name suggests, this final step enables the organizer to fill in session details to be undertaken during the event.

This essentially condensed the flow to this :

The updated wizard checklist

To implement this, the navigation needed to be altered first in the way that Forward and Previous buttons comply to the status bar steps

// app/controller/create.jsmove() {
    this.saveEventDataAndRedirectTo(
      'events.view.edit.attendee',
      ['tickets', 'socialLinks', 'copyright', 'tax', 'stripeAuthorization']
    );
  }
//app/controller/events/view/edit/sponsorship
move(direction) {
    this.saveEventDataAndRedirectTo(
      direction === 'forwards' ? 'events.view.edit.sessions-speakers' : 'events.view.edit.attendee',
      ['sponsors']
    );
  }

Once the navigation was done, I decided to add the step in the progress bar by simply including the attendees form in the event mixin.

// app/mixins/event-wizard.js
    {
      title     : this.l10n.t('Attendee Form'),
      description : this.l10n.t('Know your audience'),
      icon     : 'list icon',
      route     : 'events.view.edit.attendee'
    }

Now a basic layout for the wizard is prepared, all what is left is setting up the route for this step and including it in the router file. I took my inspiration for setting up the route from events/view/tickets/order-from.js and implemented it like this:

// app/routes/events/view/edit/attendee.js
import Route from '@ember/routing/route';
import CustomFormMixin from 'open-event-frontend/mixins/event-wizard';
import { A } from '@ember/array';
export default Route.extend(CustomFormMixin, {

titleToken() {
  return this.l10n.t('Attendee Form');
},

async model() {
  let filterOptions = [{
    name : 'form',
    op : 'eq',
    val : 'attendee'
  }];

  let data = {
    event: this.modelFor('events.view')
  };
  data.customForms = await data.event.query('customForms', {
    filter       : filterOptions,
    sort         : 'id',
    'page[size]' : 50
  });

  return data;
},
afterModel(data) {
  /**
    * Create the additional custom forms if only the compulsory forms exist.
    */
  if (data.customForms.length === 3) {
    let customForms = A();
    for (const customForm of data.customForms ? data.customForms.toArray() : []) {
      customForms.pushObject(customForm);
    }

    const createdCustomForms = this.getCustomAttendeeForm(data.event);

    for (const customForm of createdCustomForms ? createdCustomForms : []) {
      customForms.pushObject(customForm);
    }

    data.customForms = customForms;
  }
}
});

With the route setup and included in the router, I just need to take care of the form data and pass it to the server. Thankfully, the project was already using EventWizardMixin so all I had to do was utilize these functions (save and move) which saves the event data in the status user decides to save it in i.e. either published or draft state

// app/controllers/events/view/edit/attendee.js
import Controller from '@ember/controller';
import EventWizardMixin from 'open-event-frontend/mixins/event-wizard';

export default Controller.extend(EventWizardMixin, {
async saveForms(data) {
  for (const customForm of data.customForms ? data.customForms.toArray() : []) {
    await customForm.save();
  }
  return data;
},
actions: {
  async save(data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        'events.view.index',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  },
  async move(direction, data) {
    try {
      await this.saveForms(data);
      this.saveEventDataAndRedirectTo(
        direction === 'forwards' ? 'events.view.edit.sponsors' : 'events.view.edit.basic-details',
        []
      );
    } catch (error) {
      this.notify.error(this.l10n.t(error.message));
    }
  }
}
});

Apart from that, the form design was already there, essentially, I reutilized the form design provided to an event organizer / co-organizer in the ticket section of the event dashboard to make it look like this form :

Basic attendee information collection

In the end, after utilizing the existing template and adding it in the route’s template, the implementation is ready for a test run!

// app/templates/events/view/edit/attendee.hbs
{{forms/wizard/attendee-step data=model move='move' save='save' isLoading=isLoading}}

This is a simple test run of how the attendees form step works as others work fine along with it!

Demonstration of new event submission workflow

Resources

Related Work and Code Repository

Continue Reading

Dependency Injection with Kotlin Koin in Eventyay Attendee

Eventyay Attendee Android app contains a lot of shared components between classes that should be reused. Dependency Injection with Koin really comes in as a great problem solver.

Dependency Injection is a common design pattern used in various projects, especially with Android Development. In short, dependency injection helps to create/provide instances to the dependent class, and share it among other classes.

  • Why using Koin?
  • Process of setting up Koin in the application
  • Results
  • Conclusion
  • Resources

Let’s get into the details

WHY USING KOIN?

Before Koin, dependency injection in Android Development was mainly used with other support libraries like Dagger or Guice. Koin is a lightweight alternative that was developed for Kotlin developers. Here are some of the major things that Koin can do for your project:

  • Modularizing your project by declaring modules
  • Injecting class instances into Android classes
  • Injecting class instance by the constructor
  • Supporting with Android Architecture Component and Kotlin
  • Testing easily

SETTING UP KOIN IN THE ANDROID APPLICATION

Adding the dependencies to build.gradle

// Koin
implementation “org.koin:koin-android:$koin_version”
implementation “org.koin:koin-androidx-scope:$koin_version”
implementation “org.koin:koin-androidx-viewmodel:$koin_version”

Create a folder to manage all the dependent classes.

Inside this Modules class, we define modules and create “dependency” class instances/singletons that can be reused or injected. For Eventyay Attendee, we define 5 modules: commonModule, apiModule, viewModelModule, networkModule, databaseModule. This saves a lot of time as we can make changes like adding/removing/editing the dependency in one place.

Let’s take a look at what is inside some of the modules:

DatabaseModule

val databaseModule = module {

  single {
      Room.databaseBuilder(androidApplication(),
          OpenEventDatabase::class.java, “open_event_database”)
          .fallbackToDestructiveMigration()
          .build()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.eventDao()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.sessionDao()
  }

CommonModule

val commonModule = module {
  single { Preference() }
  single { Network() }
  single { Resource() }
  factory { MutableConnectionLiveData() }
  factory<LocationService> { LocationServiceImpl(androidContext()) }
}

ApiModule

val apiModule = module {
  single {
      val retrofit: Retrofit = get()
      retrofit.create(EventApi::class.java)
  }
  single {
      val retrofit: Retrofit = get()
      retrofit.create(AuthApi::class.java)
  }

NetworkModule

single {
  val connectTimeout = 15 // 15s
  val readTimeout = 15 // 15s

  val builder = OkHttpClient().newBuilder()
      .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
      .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
      .addInterceptor(HostSelectionInterceptor(get()))
      .addInterceptor(RequestAuthenticator(get()))
      .addNetworkInterceptor(StethoInterceptor())

  if (BuildConfig.DEBUG) {
      val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
      builder.addInterceptor(httpLoggingInterceptor)
  }
  builder.build()
}

single {
  val baseUrl = BuildConfig.DEFAULT_BASE_URL
  val objectMapper: ObjectMapper = get()
  val onlineApiResourceConverter = ResourceConverter(
      objectMapper, Event::class.java, User::class.java,
      SignUp::class.java, Ticket::class.java, SocialLink::class.java, EventId::class.java,
      EventTopic::class.java, Attendee::class.java, TicketId::class.java, Order::class.java,
      AttendeeId::class.java, Charge::class.java, Paypal::class.java, ConfirmOrder::class.java,
      CustomForm::class.java, EventLocation::class.java, EventType::class.java,
      EventSubTopic::class.java, Feedback::class.java, Speaker::class.java, FavoriteEvent::class.java,
      Session::class.java, SessionType::class.java, MicroLocation::class.java, SpeakersCall::class.java,
      Sponsor::class.java, EventFAQ::class.java, Notification::class.java, Track::class.java,
      DiscountCode::class.java, Settings::class.java, Proposal::class.java)

  Retrofit.Builder()
      .client(get())
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .addConverterFactory(JSONAPIConverterFactory(onlineApiResourceConverter))
      .addConverterFactory(JacksonConverterFactory.create(objectMapper))
      .baseUrl(baseUrl)
      .build()
}

As described in the code, Koin support single for creating a singleton object, factory for creating a new instance every time an object is injected.

With all the modules created, it is really simple to get Koin running in the project with the function startKoin() and a few lines of code. We use it inside the application class:

startKoin {
  androidLogger()
  androidContext([email protected])
  modules(listOf(
      commonModule,
      apiModule,
      viewModelModule,
      networkModule,
      databaseModule
  ))
}

Injecting created instances defined in the modules can be used in two way, directly inside a constructor or injecting into Android classes.  

Here is an example of dependency injection to the constructor that we used for a ViewModel class and injecting that ViewModel class into the Fragment:

class EventsViewModel(
  private val eventService: EventService,
  private val preference: Preference,
  private val resource: Resource,
  private val mutableConnectionLiveData: MutableConnectionLiveData,
  private val config: PagedList.Config,
  private val authHolder: AuthHolder
) : ViewModel() {
class EventsFragment : Fragment(), BottomIconDoubleClick {
  private val eventsViewModel by viewModel<EventsViewModel>()
  private val startupViewModel by viewModel<StartupViewModel>()

For testing, it is also really easy with support library from Koin.

@Test
fun testDependencies() {
  koinApplication {
      androidContext(mock(Application::class.java))
      modules(listOf(commonModule, apiModule, databaseModule, networkModule, viewModelModule))
  }.checkModules()
}

CONCLUSION

Koin is really easy to use and integrate into Kotlin Android project. Apart from some of the basic functionalities mention above, Koin also supports other helpful features like Scoping or Logging with well-written documentation and examples. Even though it is only developed a short time ago, Koin has proved to be a great use in the Android community. So the more complicated your project is, the more likely it is that dependency injection with Koin will be a good idea.

RESOURCES 

Documentation: https://insert-koin.io/

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Continue Reading

Omise Integration in Open Event Frontend

This blog post will elaborate on how omise has been integrated into the Open Event Frontend project. Omise is Thailand’s leading online payment gateway offering a wide range of processing solutions for this project and integrating it as a payment option widens the possibilities for user base and ease of payment workflow.

Similar to Paypal, Omise offers two alternatives for using their gateway, Test mode and Live mode, where the former is generally favoured for usage in Development and Testing phase while the latter is used in actual production for capturing live payments. Both these modes require a Public key and Secret key each and are only update-able on the admin route.

This was implemented by introducing appropriate fields in the settings model.

// app/models/setting.js
omiseMode            : attr('string'),
omiseTestPublic      : attr('string'),
omiseTestSecret      : attr('string'),
omiseLivePublic      : attr('string'),
omiseLiveSecret      : attr('string')

Once your Omise credentials are configured, you can go ahead and include the options in your event creation form. You will see an option to include Omise in your payment options if you have configured your keys correctly and if the gateway supports the currency your event is dealing with, for example, even if your keys are correctly configured, you will not get the option to use omise gateway for money collection if the currency is INR.

For showing omise option in the template, a simple computed property did the trick canAcceptOmise  in the form’s component file and the template as follows:

// app/components/forms/wizard/basic-details-step.js
canAcceptOmise: computed('data.event.paymentCurrency', 'settings.isOmiseActivated', function() {
  return this.get('settings.isOmiseActivated') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).omise;
})
// app/templates/components/forms/wizard/basic-details-step.js
{{#
if canAcceptOmise}}
      <
label>{{t 'Payment with Omise'}}</label>
      <
div class="field payments">
        <
div class="ui checkbox">
          {{input type='checkbox' id='payment_by_omise' checked=data.event.canPayByOmise}}
          <
label for="payment_by_omise">
            {{t 'Yes, accept payment through Omise Gateway'}}
            <
div class="ui hidden divider"></div>
            <
span class="text muted">
              {{t 'Omise can accept Credit and Debit Cards , Net-Banking and AliPay. Find more details '}}
              <
a href="https://www.omise.co/payment-methods" target="_blank" rel="noopener noreferrer">{{t 'here'}}</a>.
            </
span>
          </
label>
        </
div>
      </
div>
      {{#
if data.event.canPayByOmise}}
        <
label>{{t 'Omise Gateway has been successfully activated'}}</label>
      {{/
if}}
    {{/
if}}

Once the event has the payment option enabled, an attendee has chosen the option to pay up using omise, they will encounter this screen on their pending order page 

On entering the credentials correctly, they will be forwarded to order completion page. On clicking the “Pay” button, the omise cdn used hits the server with a POST request to the order endpoint  and is implemented as follows :

//controllers/orders/pending.js
isOmise: computed('model.order.paymentMode', function() {
  return this.get('model.order.paymentMode') === 'omise';
}),

publicKeyOmise: computed('settings.omiseLivePublic', 'settings.omiseLivePublic', function() {
  return this.get('settings.omiseLivePublic') || this.get('settings.omiseTestPublic');
}),

omiseFormAction: computed('model.order.identifier', function() {
  let identifier = this.get('model.order.identifier');
  return `${ENV.APP.apiHost}/v1/orders/${identifier}/omise-checkout`;
})
// app/templates/orders/pending.hbs
    {{#
if isOmise}}
      <
div>
        <
form class="checkout-form" name="checkoutForm" method='POST' action={{omiseFormAction}}>
          <
script type="text/javascript" src="https://cdn.omise.co/omise.js"
                  data-key="{{publicKeyOmise}}"
                  data-amount="{{paymentAmount}}"
                  data-currency="{{model.order.event.paymentCurrency}}"
                  data-default-payment-method="credit_card">
          </
script>
        </
form>
      </
div>
    {{/
if}}

Thus primarily using Omise.js CDN and introducing the omise workflow, the project now has accessibility to Omise payment gateway service and the organiser can see his successful charge.

Resources

Related Work and Code Repository

Continue Reading

Integrating System Roles API in Open Event Frontend

The Eventyay system supports different system roles and allows to set panel permissions for every role. The system supports two inbuilt roles namely Admin and Super Admin. The users having access to permissions panel can create new custom system roles and define set of panel permissions for them. Also the users are provided with the option of editing and deleting any system role except the two inbuilt system roles. The feature is implemented using custom-system-roles and panel-permissions API on the server.

Adding route for system-roles

The route for custom-system-system roles is defined which contains a model returning user permissions, system roles and the panel permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

async model() {
 return {
   userPermissions  : await this.get('store').findAll('user-permission'),
   systemRoles      : await this.get('store').findAll('custom-system-role'),
   panelPermissions : await this.get('store').findAll('panel-permission')
 };
},

The route created above gets all the data for user permissions, system-roles and panel permissions which is later used by the template for rendering of data.

Adding model for system-roles and panel-permissions

The model for system-roles is created which contains the ‘name’ attribute of type string and a relationship with panel permissions. Every system role can have multiple panel permissions, therefore a hasMany relationship is defined in the model.

export default ModelBase.extend({
 name: attr('string'),

 panelPermissions: hasMany('panelPermission')
});

Similarly, the model for panel-permissions is added to the models directory. The defined model contains ‘panelName’ as an attribute of type string and a bool value canAccess, defining if the panel is accessible by any role or not.

export default ModelBase.extend({
 panelName : attr('string'),
 canAccess : attr('boolean')
});

Defining controller for system-roles

The controller for system-roles is defined in the controllers/admin/permissions directory. The action for adding, updating and deleting system roles are defined in the controller. While adding the system roles, all the panels are fetched and checked which panel permissions are selected by the admin. A special property namely ‘isChecked’ is added to every panel permission checkbox which toggles on change. If the property is set true the corresponding panel is added to the panel permissions relationship of corresponding role. If no panel is selected, an error message to select atleast one panel is displayed.

deleteSystemRole(role) {
 this.set('isLoading', true);
 role.destroyRecord()
  ...
  // Notify success or failure
},
addSystemRole() {
 this.set('isLoading', true);
 let panels = this.get('panelPermissions');

 panels.forEach(panel => {
   if (panel.isChecked) {
     this.get('role.panelPermissions').addObject(panel);
   } else {
     this.get('role.panelPermissions').removeObject(panel);
   }
 });
 if (!this.get('role.panelPermissions').length) {
  // Notification to select atleast one panel
 } else {
   this.get('role').save()
    // Notify success or failure
 }
},
updatePermissions() {
 this.set('isLoading', true);
 this.get('model.userPermissions').save()
  ...
  // Notify success or failure
}

The actions defined above in the controller can be used in template by passing the appropriate parameters if required. The addSystemRole action makes a POST request to server for creating a new system role, the updatePermissions action makes a PATCH request for updating the existing system role and the deleteSystemRole action makes a delete request to the server for deleting the role.

Adding data to template for system-roles

The data obtained from the model defined in route is rendered in the template for system-roles. A loop for showing all system roles is added to the template with the name attribute containing the name of system role and another loop is added to display the panel permissions for the corresponding role.

{{#each model.systemRoles as |role|}}
 <tr>
   <td>{{role.name}}</td>
   <td>
     <div class="ui bulleted list">
       {{#each role.panelPermissions as |permission|}}
         <div class="item">{{concat permission.panelName ' panel'}}</div>
       {{/each}}
     </div>
   </td>
   <td>
    // Buttons for editing and deleting roles
   </td>
 </tr>
{{/each}}

A modal is to the component for creating and editing system roles. The data from this template is passed to the modal where the existing permissions are already checked and can be modified by the admins.

Resources

Continue Reading

Integrating Event Roles API in Open Event Frontend

The Eventyay system supports different type of roles for an event like Attendee, organizer, co-organizer, track-organizer, moderator and the registrar. Every role has certain set of permissions such as Create, Read, Update, Delete. The Admin of the system is allowed to change the permissions for any role. The interface for updating the even role permissions was already available on the server but was not integrated on the frontend. The system is now integrated with the API and allows admin to change event role permission for any role.

Adding model for event role permissions

The model for event role permissions is added to the models directory. The model contains the attributes like canDelete, canUpdate, canCreate, canRead and the relationship with event role and the service.

export default ModelBase.extend({
 canDelete : attr('boolean'),
 canUpdate : attr('boolean'),
 canCreate : attr('boolean'),
 canRead   : attr('boolean'),

 role        : belongsTo('role'),
 service     : belongsTo('service'),
 serviceName : computed.alias('service.name')
});

The above defined model ensures that every permission belongs to a role and service. An alias is declared in the model using the computed property which is later used in the controller to sort the permissions according to service name in lexicographical order.

Adding route for event roles

The route for event role is created which contains model returning an object containing the list of roles, services and permissions. The model is defined as async so that the execution is paused while fetching the data from the store by adding the await expression.

export default Route.extend({
 titleToken() {
   return this.get('l10n').t('Event Roles');
 },
 async model() {
   return {
     roles       : ['Attendee', 'Co-organizer', 'Moderator', 'Organizer', 'Track Organizer', 'Registrar'],
     services    : await this.get('store').query('service', {}),
     permissions : await this.get('store').query('event-role-permission', { 'page[size]': 30 })
   };
 }
});

The route created above queries the data for roles, services and permissions which is later used by the template for rendering of the data obtained.

Adding controller for event roles

The controller for event roles is added to the controllers/admin/permissions directory. The computed property is used to sort the services obtained from model lexicographically and the permissions are sorted by the help of alias created in the model.

services: computed('model', function() {
 return this.get('model.services').sortBy('name');
}),
sortDefinition : ['serviceName'],
permissions    : computed.sort('model.permissions', 'sortDefinition'),
actions        : {
 updatePermissions() {
   this.set('isLoading', true);
   this.get('model.permissions').save()
     .then(() => {
       // Notify success and add Error handler
      }
   }
}

An action named updatePermissions is defined which is triggered when the admin updates and saves the permissions for any role where a PATCH request is made to the server in order to update the permissions.

Rendering data in the template

The data obtained from the model is manipulated in the controller and is rendered to the table in the event-roles template. Every role is fetched from the model and added to the template, all the permissions in sorted order are obtained from the controller and matched with the current role name. The relationship of permissions with role is used to check if its title is equal to the the current role. The permissions are updated accordingly, if the role title is equal to current role.

<tbody>
 {{#each model.roles as |role|}}
   <tr>
     <td>{{role}}</td>
     {{#each permissions as |permission|}}
       {{#if (eq permission.role.titleName role)}}
         <td>
           {{ui-checkbox label=(t 'Create') checked=permission.canCreate onChange=(action (mut permission.canCreate))}}
           <br>
           {{ui-checkbox label=(t 'Read') checked=permission.canRead onChange=(action (mut permission.canRead))}}
           <br>
           {{ui-checkbox label=(t 'Update') checked=permission.canUpdate onChange=(action (mut permission.canUpdate))}}
           <br>
           {{ui-checkbox label=(t 'Delete') checked=permission.canDelete onChange=(action (mut permission.canDelete))}}
         </td>
       {{/if}}
     {{/each}}
   </tr>
 {{/each}}
</tbody>

After rendering the data as shown above, the checkbox for permissions of different services for different roles are checked or unchecked depending upon the bool value of corresponding permission. The admin can update the permissions by checking or unchecking the checkbox and saving the changes made.

Resources

Continue Reading

Adding Speakers Page in Open Event Frontend

Open Event Frontend earlier displayed all the speakers of an event on the main info page only, now a separate route for speakers is created and a separate page is added to display the speakers of an event. The design and layout of speakers page is kept similar to that on Open Event Web app. The info page only shows the featured speakers for an event and the complete list of speakers with additional information is present on speakers route.

Getting the event speakers data

The event data is obtained from the public model and a query is made for the speakers to get the required data. The speakers are fetched only for the sessions which are accepted, this is done by applying a filter while the query is made.

async model() {
 const eventDetails = this.modelFor('public');
 return {
   event    : eventDetails,
   speakers : await eventDetails.query('speakers', {
     filter: [
       {
         name : 'sessions',
         op   : 'any',
         val  : {
           name : 'state',
           op   : 'eq',
           val  : 'accepted'
         }
       }
     ]
   })
 };
}

Adding template for displaying speakers

A template is added to display three speakers in a row. The speakers data obtained from the model is looped through and details of every speaker is passed to the speaker-item component, which handles the design and layout for every item in the speakers list.

<div class="ui stackable grid container">
 {{#each model.speakers as |speaker|}}
   <div class="five wide column speaker-column">
     {{public/speaker-item speaker=speaker}}
   </div>
 {{/each}}
</div>

Adding component for speaker-item

A component for displaying the speaker-item is added to templates/component/public directory. The component contains of an accordion which displays the speaker details like biography, social links and the sessions that would be taken by him.

{{#ui-accordion}}
 <div class="title">
   <div class="ui">
     <img alt="speaker" class="ui medium rounded image" src="{{if speaker.photo.iconImageUrl speaker.image '/images/placeholders/avatar.png'}}">
    ...
    ... 
    ...
    // Speaker Details
   </div>
 </div>
{{/ui-accordion}}

The accordion with speaker image and other details appears for every speaker of an event.

Resources

Continue Reading
Close Menu