Implementation of Pagination in Open Event Organizer Android App

Pagination (Endless Scrolling or Infinite Scrolling) breaks down a list of content into smaller parts, loaded one at a time. It is important when the quantity of data to be loaded is huge and loading all the data at once can result in timeout.

Here, we will discuss about the implementation of pagination in the list of attendees in the Open Event Organizer App (Eventyay Organizer App).

It is an Android app used by event organizers to create and manage events on the Eventyay platform. Features include event creation, ticket management, attendee list with ticket details, scanning of participants etc.

In the Open Event Organizer App, the loading of attendees would result in timeout when the number of attendees would be large. The solution for fixing this was the implementation of pagination in the Attendees fragment.

First, the API call needs to be modified to include the page size as well as the addition of page number as a Query.

@GET("events/{id}/attendees?include=order,ticket,event&fields[event]=id&fields[ticket]=id&page[size]=20")
Observable<List<Attendee>> getAttendeesPageWise(@Path("id") long id, @Query("page[number]") long pageNumber);

Now, we need to modify the logic of fetching the list of attendees to include the page number. Whenever one page ends, the next page should be fetched automatically and added to the list.

The page number needs to be passed as an argument in the loadAttendeesPageWise() method in AttendeesViewModel.

public void loadAttendeesPageWise(long pageNumber, boolean forceReload) {

    showScanButtonLiveData.setValue(false);

    compositeDisposable.add(
        getAttendeeSourcePageWise(pageNumber, forceReload)
            .doOnSubscribe(disposable -> progress.setValue(true))
            .doFinally(() -> progress.setValue(false))
            .toSortedList()
            .subscribe(attendees -> {
                attendeeList.addAll(attendees);
                attendeesLiveData.setValue(attendees);
                showScanButtonLiveData.setValue(!attendeeList.isEmpty());
            }, throwable -> error.setValue(ErrorUtils.getMessage(throwable).toString())));
}

Also in the getAttendeeSourcePageWise() method:

private Observable<Attendee> getAttendeeSourcePageWise(long pageNumber, boolean forceReload) {
    if (!forceReload && !attendeeList.isEmpty())
        return Observable.fromIterable(attendeeList);
    else
        return attendeeRepository.getAttendeesPageWise(eventId, pageNumber, forceReload);
}

Now, in the AttendeesFragment, a check is needed to increase the current page number and load attendees for the next page when the user reaches the end of the list. 

if (!recyclerView.canScrollVertically(1)) {

    if (recyclerView.getAdapter().getItemCount() > currentPage * ITEMS_PER_PAGE) {
        currentPage++;
    } else {
        currentPage++;                       
        attendeesViewModel.loadAttendeesPageWise(currentPage, true);
    }
}

When a new page is fetched, we need to update the existing list and add the elements from the new page.

@Override
public void showResults(List<Attendee> attendees) {
    attendeeList.addAll(attendees);
    fastItemAdapter.setNewList(attendeeList);
    binding.setVariable(BR.attendees, attendeeList);
    binding.executePendingBindings();
}

Now, list of attendees would be fetched pagewise, thus improving the performance and preventing timeouts.

Resources:

Further reading:

Open Event Organizer App: Project repo, Play Store, F-Droid

Continue Reading Implementation of Pagination in Open Event Organizer Android App

Migration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Open Event Organizer App (Eventyay Organizer App) is the Android app used by event organizers to create and manage events on the Eventyay platform as well as check-in and check-out attendees along with other functionalities. The app used the MVP (Model-View-Presenter) architecture and is being ported to MVVM (Model-View-ViewModel). This article will explain the procedure of migrating MVP to MVVM architecture and implementing LiveData. 

Why migrate to MVVM?

The MVVM architecture is designed to store and manage UI-related data in a lifecycle conscious way. Configuration changes such as screen rotations are handled properly by ViewModels.

Tight Coupling:

The issue of tight coupling is resolved since only the View holds the reference to ViewModel and not vice versa. A single View can hold references to multiple ViewModels.

Testability:

Since Presenters are hard bound to Views, writing unit tests becomes slightly difficult as there is a dependency of a View.

ViewModels are more unit test friendly as they can be independently tested. There is no dependency of the View.

Here, the implementation is being described with the example of About Event module in the Open Event Organizer App.

First step is the creation of a new class AboutEventViewModel which extends ViewModel.

@Binds
@IntoMap
@ViewModelKey(AboutEventViewModel.class)
public abstract ViewModel bindAboutEventViewModel(AboutEventViewModel aboutEventViewModel);

The new ViewModel has to be added to the ViewModelModule:

Constructor for the ViewModel:

@Inject
public AboutEventViewModel(EventRepository eventRepository,  CopyrightRepository copyrightRepository,
DatabaseChangeListener<Copyright> copyrightChangeListener) {
    this.eventRepository = eventRepository;
    this.copyrightRepository = copyrightRepository;
    this.copyrightChangeListener = copyrightChangeListener;

    eventId = ContextManager.getSelectedEvent().getId();
}

We are using Dagger2 for dependency injection. 

LiveData

LiveData is a lifecycle-aware data holder with the observer pattern.

When we have a LiveData object (e.g. list of attendees), we can add some LifecycleOwner (it can be Activity or Fragment) as an observer. Using this:

The Activity or Fragment will remain updated with the data changes.

Observers are only notified if they are in the STARTED or RESUMED state which is also known as the active state. This prevents memory leaks and NullPointerExceptions because inactive observers are not notified about changes.

Now, let’s discuss about the implementation of LiveData. We will create objects of SingleEventLiveData<> class.

private final SingleEventLiveData<Boolean> progress = new SingleEventLiveData<>();
private final SingleEventLiveData<String> error = new SingleEventLiveData<>();
private final SingleEventLiveData<Event> success = new SingleEventLiveData<>();
private final SingleEventLiveData<Copyright> showCopyright = new SingleEventLiveData<>();
private final SingleEventLiveData<Boolean> changeCopyrightMenuItem = new SingleEventLiveData<>();
private final SingleEventLiveData<String> showCopyrightDeleted = new SingleEventLiveData<>();

The functions to get the LiveData objects:

public LiveData<Boolean> getProgress() {
    return progress;
}

public LiveData<Event> getSuccess() {
    return success;
}

public LiveData<String> getError() {
    return error;
}

public LiveData<Copyright> getShowCopyright() {
    return showCopyright;
}

public LiveData<Boolean> getChangeCopyrightMenuItem() {
    return changeCopyrightMenuItem;
}

public LiveData<String> getShowCopyrightDeleted() {
    return showCopyrightDeleted;
}

Now, we can remove getView() methods and instead, these objects will be used to call various methods defined in the fragment.

Let’s discuss the changes required in the AboutEventFragment now.

The Fragment will have ViewModelProvider.Factory injected.

@Inject
ViewModelProvider.Factory viewModelFactory;

Declare an object of the ViewModel.

private AboutEventViewModel aboutEventViewModel;

Then, in onCreateView(), viewModelFactory will be passed to the ViewModelProviders.of() method as the factory, which is the second parameter.

aboutEventViewModel = ViewModelProviders.of(this, viewModelFactory).get(AboutEventViewModel.class);

Replace all references to the Presenter with references to the ViewModel.

Add the Fragment as an observer to the changes by adding the following in the onStart() method:

aboutEventViewModel.getProgress().observe(this, this::showProgress);
aboutEventViewModel.getSuccess().observe(this, this::showResult);
aboutEventViewModel.getError().observe(this, this::showError);
aboutEventViewModel.getShowCopyright().observe(this, this::showCopyright);
aboutEventViewModel.getChangeCopyrightMenuItem().observe(this, this::changeCopyrightMenuItem);
aboutEventViewModel.getShowCopyrightDeleted().observe(this, this::showCopyrightDeleted);

Two parameters are passed to the observe() method  –  first one is LifecycleOwner, which is our Fragment in this case. The second one is a callback along with a parameter and is used to call the required method.

With this, the implementation of MVVM and LiveData is brought to completion.

Resources:

Documentation: ViewModel, LiveData

Further reading:

Open Event Organizer App: Project repo, Play Store, F-Droid

Continue Reading Migration to Model-View-ViewModel Architecture and LiveData in Open Event Organizer App

Implementing Event Invoice Forms

  • Post author:
  • Post category:FOSSASIA

This blog post elaborates on the recent addition of user billing form in Eventyay which is an open source event management solution which allows users to buy & sell tickets, organize events & promote their brand, developed by FOSSASIA. As this project moves forward with the implementation of event invoices coming up,. In the past few weeks, I have collaborated with fellow developers in planning the integration of event invoice payments and this is a necessary step for the same due to its involvement in order invoice templates. This implementation focuses on event invoices billing ( the calculated amount an event organiser has to pay to the platform for their event’s revenue ).

This form includes basic details like contact details, tax ID, billing location and additional information (if any). The following is a specimen of this form :

Tax Form Implementation

First step of this form creation is to employ the account/billing/payment-info route for serving the relevant model data to the frontend.

// app/routes/account/billing/payment-info.js
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default class extends Route.extend(AuthenticatedRouteMixin) {
titleToken() {
  return this.l10n.t('Payment Info');
}
}

Since the field additions have been done in the user schema in the server side, the corresponding changes have to made in the ember user model as well.

// app/models/user.js
/**
  * Billing Contact Information
  */

billingContactName    : attr('string'),
billingPhone          : attr('string'),
billingCountry        : attr('string'),
company               : attr('string'),
billingAddress        : attr('string'),
billingCity           : attr('string'),
billingZipCode        : attr('string'),
billingTaxInfo        : attr('string'),
billingAdditionalInfo : attr('string'),
billingState          : attr('string'),

This form has a speciality. Instead of using the current user information directly, it uses an intermediate object and employs manipulation in current user record only when the submit button is clicked. This has been implemented in the following way : 

// app/components/user-payment-info-form.js
export default class extends Component.extend(FormMixin) {
didInsertElement() {
  super.didInsertElement(...arguments);
  this.set('userBillingInfo', pick(this.authManager.currentUser, ['billingContactName', 'billingCity', 'billingPhone', 'company', 'billingTaxInfo', 'billingCountry', 'billingState', 'billingAddress', 'billingZipCode', 'billingAdditionalInfo']));
}
@action
submit() {
  this.onValid(async() => {
    this.set('isLoading', true);
    try {
      this.authManager.currentUser.setProperties(this.userBillingInfo);
      await this.authManager.currentUser.save();
      this.notify.success(this.l10n.t('Your billing details has been updated'));
    } catch (error) {
      this.authManager.currentUser.rollbackAttributes();
      this.notify.error(this.l10n.t('An unexpected error occurred'));
    }
    this.set('isLoading', false);
  });
}
}

The usual form validations are employed as expected in this one too and works well in storing the invoice based information.

Resources

Related Work and Code Repository

Continue Reading Implementing Event Invoice Forms

Implement Order Confirmation Feature in Eventyay

  • Post author:
  • Post category:FOSSASIA

This post elaborates on the details of an endpoint which can be used to explicatively used to resend order confirmations. In the current implementation of the open event project, if the order has been confirmed, the ticket holders and buyers get an email each regarding their order confirmation. But in case that email has been accidentally deleted by any of the attendees, the event organizer / owner should have the power to resend the confirmations.

The first step to the implementation was to create the appropriate endpoint for the server to be pinged. I utilized the existing blueprint being used for serving tickets on eventyay frontend project and created a new endpoint on the route : orders/resend-email [POST]

# app/api/auth.py
@ticket_blueprint.route('/orders/resend-email', methods=['POST'])
@limiter.limit(
  '5/minute', key_func=lambda: request.json['data']['user'], error_message='Limit for this action exceeded'
)
@limiter.limit(
  '60/minute', key_func=get_remote_address, error_message='Limit for this action exceeded'
)
def resend_emails():
  """
  Sends confirmation email for pending and completed orders on organizer request
  :param order_identifier:
  :return: JSON response if the email was succesfully sent
  """
  order_identifier = request.json['data']['order']
  order = safe_query(db, Order, 'identifier', order_identifier, 'identifier')
  if (has_access('is_coorganizer', event_id=order.event_id)):
      if order.status == 'completed' or order.status == 'placed':
          # fetch tickets attachment
          order_identifier = order.identifier
          key = UPLOAD_PATHS['pdf']['tickets_all'].format(identifier=order_identifier)
          ticket_path = 'generated/tickets/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'
          key = UPLOAD_PATHS['pdf']['order'].format(identifier=order_identifier)
          invoice_path = 'generated/invoices/{}/{}/'.format(key, generate_hash(key)) + order_identifier + '.pdf'

          # send email.
          send_email_to_attendees(order=order, purchaser_id=current_user.id, attachments=[ticket_path, invoice_path])
          return jsonify(status=True, message="Verification emails for order : {} has been sent succesfully".
                          format(order_identifier))
      else:
          return UnprocessableEntityError({'source': 'data/order'},
                                          "Only placed and completed orders have confirmation").respond()
  else:
      return ForbiddenError({'source': ''}, "Co-Organizer Access Required").respond()

I utilized exiting send_email_to_attendees for the email purpose but for security reasons, the endpoint was limited to make sure that an organizer can request only 5 order confrimations to be resent each minute (implemented using flask limiter).

This was all for server implementation, to implement this on the front end, I just created a new action named as resendConfirmation implemented as given.

// app/controllers/events/view/tickets/orders/list.js
async resendConfirmation(order) {
    let payload = {};
    try {
      payload = {
        'data': {
          'order' : order.identifier,
          'user'  : this.authManager.currentUser.email
        }
      };
      await this.loader.post('orders/resend-email', payload);
      this.notify.success(this.l10n.t('Email confirmation has been sent to attendees successfully'));
    } catch (error) {
      if (error.status === 429) {
        this.notify.error(this.l10n.t('Only 5 resend actions are allowed in a minute'));
      }
      if (error.errors[0].detail) {
        this.notify.error(this.l10n.t(error.errors[0].detail));
      }
    }
  }

Using a simple post request, this was implemented on the frontend for sending the confirmation, but the additional work to be done was to handle the new error (429 status). The server throws the error but loader service hasn’t been configured yet to handle this error appropriately.

// app/services/loader.js
  if (!response.ok) {
    const defaultMessage = httpStatus[response.status];
    if (parsedResponse) {
      throw parsedResponse;
    }
    if (response.status === 429) {
      throw { status: 429, message: ‘TOO MANY REQUESTS’ };
    }
    throw new Error(
      getErrorMessage(
        response.statusText,
        defaultMessage
          ? `${response.status} – ${defaultMessage}`
          : `Could not make ${fetchOptions.type} request to ${fetchOptions.url}`
      )
    );
  }

The loader service has been modified in the following manner to accommodate the new error been thrown so that a more user friendly error could be shown on the controller level.

This was the whole mechanism which has been implemented for this particular problem. 

Resources

Related Work and Code Repository

Continue Reading Implement Order Confirmation Feature in Eventyay

Enhancing Network Requests by Chaining or Zipping with RxJava

In Eventyay Attendee, making HTTP requests to fetch data from the API is one of the most basic techniques used. RxJava comes in as a great method to help us making asynchronous requests and optimize the code a lot. This blog post will deliver some advanced RxJava used in Eventyay Attendee.

  • Why using RxJava?
  • Advanced RxJava Technique – Chaining network calls with RxJava
  • Advanced RxJava Technique – Merging network calls with RxJava
  • Conclusions
  • Resources

WHY USING RXJAVA?

There are many reasons why RxJava is a great API in Android Development. RxJava is an elegant solution to control data flow in programming, where developers can cache data, get data, update the UI after getting the data, handle asynchronous tasks. RxJava also works really well with MVVM architectural pattern.

CHAINING NETWORK CALLS WITH RXJAVA

Chaining RxJava is a technique using flatMap() operator of Rxjava. It will use the result from one network call in order to make the next network call. 

In Eventyay Attendee, this technique is used when we want to update the user profile image. First, we need to upload the new profile image to the server in order to get the image URL, and then we use that URL to update the user profile

compositeDisposable += authService.uploadImage(UploadImage(encodedImage)).flatMap {
   authService.updateUser(user.copy(avatarUrl = it.url))
}.withDefaultSchedulers()
   .doOnSubscribe {
       mutableProgress.value = true
   }
   .doFinally {
       mutableProgress.value = false
   }
   .subscribe({
       mutableMessage.value = resource.getString(R.string.user_update_success_message)
       Timber.d("User updated")
   }) {
       mutableMessage.value = resource.getString(R.string.user_update_error_message)
       Timber.e(it, "Error updating user!")
   }

In conclusion, zipping RxJava helps to make HTTP requests more continuous and reduce unnecessary codes. 

ZIPPING NETWORK CALLS WITH RXJAVA

Zipping RxJava is a technique using zip() operator of Rxjava. It will wait for items from two or more Observables to arrive and then merge them together for emitting. This technique would be useful when two observables emit the same type of data.

In Eventyay Attendee, this technique is used when fetching similar events by merging events in the same location and merging events in the same event type.

var similarEventsFlowable = eventService.getEventsByLocationPaged(location, requestedPage, 3)
if (topicId != -1L) {
   similarEventsFlowable = similarEventsFlowable
       .zipWith(eventService.getSimilarEventsPaged(topicId, requestedPage, 3),
           BiFunction { firstList: List<Event>, secondList: List<Event> ->
               val similarList = mutableSetOf<Event>()
               similarList.addAll(firstList + secondList)
               similarList.toList()
           })
}

compositeDisposable += similarEventsFlowable
   .take(1)
   .withDefaultSchedulers()
   .subscribe({ response ->
       ...
   }, { error ->
       ...
   })

In conclusion, zipping RxJava helps running all the tasks in parallel and return all of the results in a single callback.

CONCLUSION

Even though RxJava is pretty hard to understand and master, it is a really powerful tool in Android Development and MVVM models. These techniques above are really simple to implement and they could improve the app by r

RESOURCES

Eventyay Attendee Source Code: 

https://github.com/fossasia/open-event-attendee-android/pull/2010

https://github.com/fossasia/open-event-attendee-android/pull/2117

RxJava Documentation: http://reactivex.io/documentation

Continue Reading Enhancing Network Requests by Chaining or Zipping with RxJava

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 Implementing Stripe payment in Eventyay Attendee

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 Adding time counter on ordering tickets in Eventyay Attendee

Tax Information on Public Ticket Page

  • Post author:
  • Post category:FOSSASIA

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 Tax Information on Public Ticket Page

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 places autosuggestion with Mapbox for searching events in Eventyay Attendee

Implementing Attendee Forms in Wizard of Open Event Frontend

  • Post author:
  • Post category:FOSSASIA

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 Implementing Attendee Forms in Wizard of Open Event Frontend