GG: The Journey of Building and Marketing an App
How I went from an idea to a published app with over 12k users - All for free!
Introduction
GG: Game Giveaways Notifier is a passion project of mine. The app shows you a comprehensive list of running game giveaways and notifies you through email (opt-in) and push notifications whenever a game goes free.
Current feature list:
Instant Notifications π²: Never miss a free game again! Get push notifications whenever a game goes free.
Comprehensive List of Giveaways π: From free games to loot, coins, skins, and battle passes, see a detailed list of giveaways with instructions on how to redeem them.
Save Money and Compete π: Keep track of how much money you've saved through the app and compete in the leaderboard. Bragging rights guaranteed! The top user in the leaderboard has saved more than $6000 through the app.
Clean and Minimal UI π¨: Enjoy a nice, clean, and easy-to-navigate user interface.
All Storefronts Covered πͺ: Whether it's Epic Games, Steam, itch.io, or others, the app got you covered.
Open Source and Actively Maintained π»: GG is open source and constantly updated.
Email Notifications π§: Soon, you'll be able to receive notifications via email too! It's a toggle in the app and the feature is complete. Just ironing out some bugs.
Conceptualizing the app
Idea
I'm a game hoarder and I've managed to get more than 500 games for free through giveaways and saved thousands of dollars. This obsession began when I discovered the EPIC! games store, which offers a free game every week. I religiously logged in to redeem each giveaway, but occasionally, life got in the way, and I missed out on some incredible titles. Frustrated by this recurring problem, I was venting with my friend, and that's when the seed of an idea was planted in my mind. While competing to see who could accumulate the most free games, we both realized the need for a solution that would ensure we never missed out on a giveaway again. It was during one of our friendly gaming battles that my friend suggested I create an app to solve this problem once and for all. The idea resonated with me, and I couldn't shake off the excitement of bringing this concept to life.
Research
Before diving headfirst into the development of the app, thorough research was essential to determine its feasibility. My primary objective was to find a reliable source that could provide a comprehensive list of ongoing game giveaways. Initially, I considered web scraping as a potential solution, but the daunting task of implementing custom scrapers for each storefront made me reconsider. Another approach I contemplated was fetching Twitter posts with the hashtag "giveaway." However, this method proved to be problematic, as the results were often flooded with scams and unreliable information. In hindsight, I'm grateful that I didn't pursue this direction, especially considering that Twitter has recently implemented a paywall for their API. After extensive exploration, I stumbled upon the ideal solution that met all my requirements: the gamerpower.com API. This elegant and user-friendly API provided a ready-made solution for accessing a comprehensive list of running giveaways. Not only did it save me from the complexities of web scraping or unreliable Twitter searches, but it also aligned perfectly with the vision I had for the app (PS. they even featured my app in their API section).
Choosing the right technologies
I wanted the app to be cross-platform. Since I already knew Flutter, that's what I've chosen as the app development framework. I'm using Firebase to authenticate users and store their data. An express.js server hosted on Railway as my notification service. And recently, I've added Admob to show ads to users, and Shorebird for automatic code push.
Building GG: The Development Process
Mobile App
I used Flutter to build the application. The development journey couldn't have been smoother. I was able to ship super fast without ever feeling like I was limited by the framework. I used a Domain Driven Design pattern to develop the app. This made the code more maintainable. Everything was separated feature-wise. The app also uses Flutter Bloc to manage state.
Database
The database that powers GG, is Firestore Database. This NoSQL database served as the backbone for storing user information and tracking their savings through the app. The leaderboard page utilized a paginated query to showcase the rankings of all users, providing a dynamic and engaging experience.
Notification API
To ensure timely notifications for new giveaways, I implemented a custom notification server. This server, running on a cron job, checks for new giveaways every hour and sends push notifications to users. This seamless integration allows users to stay up-to-date with the latest opportunities to save money on games. Here's how it works:
The code is provided at the bottom of this article if you're interested.
Testing and Refining GG
During the initial stages of development, I included a section in the app where users could directly email me to report bugs or request new features. This user feedback was invaluable as it provided real-life use cases that no amount of testing could fully anticipate. It allowed me to address issues and make improvements based on actual user experiences. However, as the app gained popularity and attracted a significant user base, the influx of emails became overwhelming. Consequently, I made the decision to remove the direct email reporting feature from the app. Since then, I've replaced email reporting with two things: Automated crash reporting through Firebase Crashlytics. This transition proved to be a pivotal change, as it provided me with detailed stack traces and crash reports. With this information at hand, I could quickly identify and address issues, ensuring a smoother and more stable user experience. I also added a Tally form to let users submit a bug report through a form.
Launching GG: Marketing and Promotion
Grassroots and Organic Marketing
From the very beginning, the marketing and promotion strategy for GG has been grassroots and organic, relying on the power of online communities and word-of-mouth. Traditional advertising has not been a part of the strategy, as the focus has been on engaging directly with gamers in their online spaces.
Engaging with Online Communities
To connect with the gaming community, I actively participate in relevant subreddits, gaming websites, and social media platforms. By immersing myself in these communities, I have been able to understand the needs of users and continuously improve GG based on their feedback. Reddit, in particular, has proven to be an effective platform for sharing GG in gaming, giveaway, and app development subreddits, sparking discussions and gaining valuable insights.
Harnessing the Power of Social Media
Social media platforms like Twitter and Facebook have played a crucial role in promoting GG. Through these channels, I keep users updated on the latest giveaways, provide sneak peeks of upcoming features, and foster friendly competition on the leaderboard. This direct engagement with the user base has helped build a loyal community around GG.
Future Marketing Strategies
Looking ahead, I am excited to explore new avenues for connecting with the gaming community and spreading the word about GG. I am considering paid promotions to gauge their effectiveness in reaching a wider audience and driving app adoption.
Performance & Metrics
Monitoring App Performance and User Engagement
Since the initial launch of GG, I have placed great importance on monitoring the app's performance and user engagement. To achieve this, I utilize Google Analytics to track various metrics such as monthly active users, total engagement time, ad clicks, and more. This data provides valuable insights into user behavior and helps me make informed decisions to enhance the app's performance and user experience.
Tracking User Acquisition and Geographical Performance
One key aspect of monitoring app performance is tracking user acquisition and understanding where our users are coming from. Google Analytics allows me to analyze the sources of app installations and identify the most effective channels for user acquisition. Additionally, it provides insights into the geographical performance of the app, enabling me to identify countries where GG is performing exceptionally well.
Localization for Targeted Countries
To further optimize user acquisition and engagement, I have implemented localizations for the top-performing countries. By tailoring the app's content and language to specific regions, I have been able to attract more users and provide a more personalized experience. This strategic approach has proven effective in expanding the user base and increasing overall engagement.
Lessons Learned & Future Plans
The journey of developing GG: Games and Giveaways has been a valuable learning experience. Throughout the process, I gained insights into various aspects of app development, user engagement, and problem-solving. However, there were also challenges along the way. One significant hurdle was when the app was unexpectedly removed from the Play Store due to a copyright claim regarding the app icon. Despite using an icon from Icons8 and crediting the artist, the app was removed without prior notice. This incident resulted in the loss of traction and a setback for the app. Looking ahead, however, I have exciting plans to further enhance GG and expand its user base. Some of the features that are already in the pipeline include:
More Localization Support: To cater to a wider audience, I plan to add additional localization support, ensuring that the app is accessible and user-friendly for users from different regions.
In-App Purchases to Remove Ads: To provide users with an ad-free experience and support the sustainability of the app, I will introduce in-app purchases that allow users to remove ads.
Enhanced User Interaction: To foster a sense of community and engagement, I am planning to introduce features that enable users to interact with each other. This will create a more dynamic and social experience within the app.
Conclusion
GG: Game Giveaways Notifier has been a labor of love from day one. I've never thought of making money while developing it. I don't know how many people use their own apps, but I've been using GG since day one. I'm super proud of what I was able to achieve with it. It has been a rewarding experience to see the app grow and evolve, and I hope that you find it as valuable and enjoyable as I do. I would greatly appreciate any feedback you may have on both the blog series and the app itself. Your input is invaluable in helping me improve and refine GG. Thank you for taking the time to read through this blog series, and I look forward to hearing from you.
Try out the app here: GG: Game Giveaways Notifier
If you want to connect with me, you can find my socials here: ikramhasan.com
The custom notification service:
import express, { Express } from 'express'
import cron from 'node-cron'
import fetch from 'node-fetch'
import admin from 'firebase-admin'
import serviceAccount from '../serviceAccountKey.json' assert { type: 'json' }
import { Resend } from 'resend'
const app: Express = express()
const port: string | number = process.env.PORT || 3000
interface Schema {
collection: string;
doc: string;
map: (data: any) => any;
}
interface NotifyMethod {
notify: (data: any) => void;
}
class CustomLogger {
isLoggingEnabled: boolean;
constructor() {
this.isLoggingEnabled = process.env.NODE_ENV === 'development';
}
log(message: string, data?: any): void {
if (this.isLoggingEnabled) {
console.log(`[${new Date().toISOString()}] ${message}`, data);
}
}
error(message: string, error: any): void {
if (this.isLoggingEnabled) {
console.error(`[${new Date().toISOString()}] ${message}`, error);
}
}
}
class NotifyService {
apiUrl: string;
schema: Schema;
notifyMethods: NotifyMethod[];
logger: CustomLogger;
db: FirebaseFirestore.Firestore;
constructor(apiUrl: string, schema: Schema, notifyMethods: NotifyMethod[], logger: CustomLogger) {
this.apiUrl = apiUrl;
this.schema = schema;
this.notifyMethods = notifyMethods;
this.logger = logger;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
this.db = admin.firestore()
}
async notify(): Promise<void> {
try {
const response = await fetch(this.apiUrl)
const dataList: any[] = await response.json() as any
const latestData = dataList[0]
const dataFromDB = (await this.db.collection(this.schema.collection).doc(this.schema.doc).get()).data()
if (dataFromDB && dataFromDB.id === latestData.id) {
this.logger.log('Data already updated')
} else {
await this.db.collection(this.schema.collection).doc(this.schema.doc).update(this.schema.map(latestData))
this.logger.log('Updated data', latestData)
// Send notification
for (const method of this.notifyMethods) {
method.notify(latestData);
}
}
} catch (error) {
this.logger.error('Error in notify function', error)
}
}
start(interval = '0 */12 * * *'): void {
cron.schedule(interval, () => {
this.logger.log('Started a new job.')
this.notify().catch((error) => this.logger.error('Error in cron job', error))
})
app.listen(port, () => {
this.logger.log(`App listening on port ${port}`)
})
}
}
class FirebaseNotifyMethod implements NotifyMethod {
topic: string;
logger: CustomLogger;
constructor(topic = 'all', logger: CustomLogger) {
this.topic = topic;
this.logger = logger;
}
notify(data: any): void {
const message = {
notification: {
title: data.title,
body: data.description,
image: data.thumbnail,
},
topic: this.topic,
}
admin
.messaging()
.send(message)
.then((response) => {
this.logger.log('Successfully sent message:', response)
})
.catch((error) => {
this.logger.error('Error sending message:', error)
})
}
}
interface EmailConfig {
resendKey: string;
from: string;
to: string;
}
class EmailNotifyMethod implements NotifyMethod {
resend: Resend;
from: string;
to: string;
constructor(emailConfig: EmailConfig) {
this.resend = new Resend(emailConfig.resendKey);
this.from = emailConfig.from;
this.to = emailConfig.to;
}
notify(data: any): void {
this.resend.emails.send({
from: this.from,
to: this.to,
subject: `Notification: ${data.title}`,
html: `<p>${JSON.stringify(data)}</p>`,
attachments: [
{
filename: 'thumbnail.png',
path: data.thumbnail,
},
],
})
}
}
// Usage
const logger = new CustomLogger();
const service = new NotifyService(
'https://www.gamerpower.com/api/giveaways',
{
collection: 'giveaways',
doc: 'notify',
map: (data: any) => ({
id: data.id,
title: data.title,
description: data.description,
thumbnail: data.thumbnail,
open_giveaway: data.open_giveaway,
}),
},
[
new FirebaseNotifyMethod('all', logger),
new EmailNotifyMethod({
resendKey: 'RESEND_API_KEY_HERE',
from: 'no-reply@ikramhasan.com',
to: '[EMAIL_SUBSCRIBERS_LIST]',
}),
],
logger
);