DEV Community

Cover image for How I Tried to Save My Google Play Developer Account — and Ended Up Making a App for Crypto
Wlad Radchenko
Wlad Radchenko

Posted on

How I Tried to Save My Google Play Developer Account — and Ended Up Making a App for Crypto

I got a surprise email from Google recently:

“Your developer account is inactive and may be closed…”

I created this account back when I was still a student, mainly to publish a couple of small side projects. Then I kind of forgot about it. Years passed. Now Google warned me I had 60 days to publish something new — or the account would be deleted. And honestly? I didn’t want to lose it. It held a little piece of my dev journey.

"Okay," I thought. "I’ll whip something up real quick. Should take, like, 10 minutes!"

And that’s how I ended up building a full open-source crypto portfolio app — with donuts and LLM generated prompts. Here’s how it all spiraled out of control.

From Quick Fix to Full-on Crypto App

My first plan? Just update an old app. But I had forgotten how to React Native. Then I figured, hey, maybe I’ll add some features. A bit of design polish. A few UX tweaks…

One weekend later, I’d rebuilt the UI from scratch, rewritten all the logic, and ended up with a neat little open-source app that builds randomized crypto portfolios and generates ChatGPT prompts for each coin.

How It Works

  • The app fetches crypto data from the CoinGecko API
  • Random coins are selected and diversified based on how much money you have
  • Each coin is displayed as a donut chart
  • You tap a button and get a prompt for ChatGPT or any LLM, like:
    • What's the investment risk level for this token (low/medium/high) and why?
    • Does it have good profit potential?
    • Is now a good time to buy, or should I wait?
    • How reliable/safe is the token based on its market cap, volume, and supply structure?
    • Give a short, actionable recommendation.

And yep, the app saves your portfolio history and lets you share it with friends. I was inspired during the creation by this picture:

I was inspired during the creation by this picture

Under the Hood

⚠️ Warning: there's a lot of stream-of-consciousness code below. If you want to know what came out of it, scroll to the end of the article.

What was going through my head while coding:

What was going through my head while coding

I’ll walk you through how I built it, the traps I ran into with Expo + React Native, and whether I actually managed to save my developer account in the end.

Requirements

You’ll need:

  • Node.js (v14+)
  • Android Studio (for Android) or Xcode (for iOS)
  • React Native with Expo
  • And EAS Build for creating Android bundles
  • The full source is on GitHub.

If you’re a total npm newbie, don’t worry — just run:

git clone <project>
сd <project>
npm install
npm run android or npm run ios
Enter fullscreen mode Exit fullscreen mode

Goodbye Old Splash Screen

The first thing I ditched was the outdated splash screen. I replaced it with a clean background and a login screen. Installed the needed packages and created a new SplashScreen.tsx.

npm install react-native-responsive-screen
Enter fullscreen mode Exit fullscreen mode

And code in SplashScreen.tsx:

import { useRef } from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity, Animated } from 'react-native';
import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen';

const SplashScreen = ({ onHide }) => {
  const fadeAnim = useRef(new Animated.Value(1)).current;
  const scaleAnim = useRef(new Animated.Value(1)).current;

  // Animation after clicking a button
  const handleGetStarted = () => {
    Animated.parallel([
        Animated.timing(fadeAnim, {
        toValue: 0,
        duration: 500,
        useNativeDriver: true,
        }),
        Animated.timing(scaleAnim, {
        toValue: 2, // you can reduce it to 0.5 or 0.1 if you want a collapse, but I have an increase
        duration: 500,
        useNativeDriver: true,
        }),
    ]).start(() => onHide());
   };

  // Draw View  
  return (
    <Animated.View style={[styles.container, { opacity: fadeAnim, transform: [{ scale: scaleAnim }] }]}>
      <View>
        <Image 
            source={require('./assets/donuts/background.png')} 
            style={{ marginTop: hp('4.76%'), width: wp('109%'), height: hp('57.14%') }}
            resizeMode="contain">
        </Image>
      </View>
      <View style={styles.content}>
        <Text style={styles.title}>Doughfolio</Text>
        <Text style={styles.description}>
          Visualize your crypto portfolio with delicious donut charts
        </Text>
      </View>
      <TouchableOpacity style={styles.button} onPress={handleGetStarted}>
        <Text style={styles.buttonText}>Get Started</Text>
      </TouchableOpacity>
    </Animated.View>
  );
};


const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: '#FFD8DF',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 1000,
  },
  content: {
    flex: 1,
    justifyContent: 'flex-end',
    alignItems: 'flex-start',
    paddingHorizontal: wp('7.27%'),
    marginBottom: hp('7.61%'),
  },
  title: {
    fontSize: wp('14%'),
    fontWeight: 'bold',
    color: '#FF6E76',
    marginBottom: hp('1.9%'),
    textAlign: 'left'
  },
  description: {
    fontSize: wp('5.09%'),
    color: '#FF6E76',
    textAlign: 'left',
  },
  button: {
    backgroundColor: 'white',
    borderRadius: 28,

    // Shadow for Android
    elevation: 10,

    // Shadow for iOS
    shadowColor: '#9B8084',
    shadowOffset: { width: 5, height: 5 },
    shadowOpacity: 0.1,
    shadowRadius: 10,

    paddingHorizontal: wp('14.72%'),
    paddingVertical: hp('2%'),
    width: '80%',
    marginBottom: hp('5.71%'),
    alignItems: 'center',
    justifyContent: 'center'
  },
  buttonText: {
    color: 'black',
    textTransform: 'uppercase',
    fontSize: wp('4.72%'),
    fontWeight: 'bold',
  },
});

// We set export to export SplashScreen to App.tsx
export default SplashScreen;
Enter fullscreen mode Exit fullscreen mode

Feeling fancy? You can even add Lottie animations. Just grab a JSON animation from LottieFiles (you'll need an account), install Lottie, and embed the animation in your view.

npm install lottie-react-native
Enter fullscreen mode Exit fullscreen mode

And integrate LottieView:

<LottieView source={require('./assets/animation.json')} autoPlay loop />
Enter fullscreen mode Exit fullscreen mode

In App.tsx, I layered the SplashScreen over the main content and used splashVisible state to toggle its visibility.

export default function App() {
  const [splashVisible, setSplashVisible] = useState(true);

  const [fontsLoaded] = useFonts({
    'Roboto-Bold': require('./src/assets/fonts/Roboto-Bold.ttf'),
    'Roboto-Light': require('./src/assets/fonts/Roboto-Light.ttf'),
  });

  return (
    <View style={{ flex: 1 }}>
      <DonutChartContainer />
      {splashVisible && (
        <SplashScreen onHide={() => setSplashVisible(false)} />
      )}
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Coin Data + Donuts

The interesting part starts in App.tsx, where the app fetches data:

// Function for getting data from CoinGecko API
async function fetchCryptoData() {
  const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=200&page=1');
  const data = await response.json();
  return data;
}

// Function to randomly select 10 cryptocurrencies
function getRandomCryptos(data, count = 10, maxIndex = 200) {
  const minSlice = Math.floor(Math.random() * (maxIndex - count + 1));  // from 0 to 90
  const maxSlice = minSlice + count;
  return data.slice(minSlice, maxSlice);
}

 const handleHistorySelect = (item: any) => {
  setData(item.data);
  // Updating totalValue via withTiming for smooth animation
  totalValue.value = withTiming(item.totalValue, { duration: 500 });
  // Recalculating percentages
  decimals.value = item.data.map(crypto => crypto.percentage / 100);
};

  ////////////////////////////
 // BREAKING THE TEMPLATE //
///////////////////////////

try {
  // Mix up the donut pictures
  setImages(getShuffledDonutImages());

  // Step 1: Get data from the API
  const cryptoData = await fetchCryptoData();

  // Step 2: Randomly select 10 cryptocurrencies
  const selectedCryptos = getRandomCryptos(cryptoData, 10);

  // Step 3: Generate random numbers for weight distribution
  const generateNumbers = generateRandomNumbers(n, amount);

  // Calculate the total sum of these numbers
  const total = generateNumbers.reduce((acc, currentValue) => acc + currentValue, 0);

  // Calculate percentages for each number
  const generatePercentages = calculatePercentage(generateNumbers, total);

  // Round off percentages and make them in the format 0.00
  const generateDecimals = generatePercentages.map((number) => {
    if (number != null && !isNaN(number)) {
      return Number(number.toFixed(0)) / 100;
    }
    return 0; // Yes, there can be null values ​​from the API
  });

  totalValue.value = withTiming(total, { duration: 1000 });

  decimals.value = [...generateDecimals];

  // Generate an array of objects with data
  const arrayOfObjects = generateNumbers.map((value, index) => ({
    name: selectedCryptos[index].name,
    image: selectedCryptos[index].image,
    symbol: selectedCryptos[index].symbol,
    minPrice: selectedCryptos[index].ath,
    maxPrice: selectedCryptos[index].atl,
    price: selectedCryptos[index].current_price,
    marketCap: selectedCryptos[index].market_cap,
    marketCapChangePercentage24h: selectedCryptos[index].market_cap_change_percentage_24h,
    priceChangePercentage24h: selectedCryptos[index].price_change_percentage_24h,
    circulatingSupply: selectedCryptos[index].circulating_supply,
    maxSupply: selectedCryptos[index].max_supply,
    totalVolume: selectedCryptos[index].total_volume,
    value,
    percentage: generatePercentages[index],
    decimals: generateDecimals[index] / 100,
    color: colors[index], // Random color generation
    url: 'https://www.coingecko.com/en/coins/' + selectedCryptos[index].id,
  }));

  // Output data to the console
  setData(arrayOfObjects);
  await addToHistory(arrayOfObjects); // Save data + total amount
} catch (error) {
  console.error('Failed to data generation:', error);
}
Enter fullscreen mode Exit fullscreen mode

When setData is called, we’re updating the data state inside the DonutChartContainer — which then redraws the chart.

const [data, setData] = useState<Data[]>([]);
Enter fullscreen mode Exit fullscreen mode

When you see a lot of code

Bonus: there’s also a useHistory.ts hook that saves every generated portfolio to AsyncStorage under the key cryptoDonutHistory. Firstly installation:

npm install @react-native-async-storage/async-storage
Enter fullscreen mode Exit fullscreen mode

And code of useHistory.ts:

// src/hooks/useHistory.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect } from 'react';

interface HistoryItem {
  date: string;
  data: any[];
  totalValue: number; 
}

export const useHistory = () => {
  const [history, setHistory] = useState<HistoryItem[]>([]);

  const loadHistory = async () => {
    try {
      const saved = await AsyncStorage.getItem('cryptoDonutHistory');
      if (saved) {
        const parsed = JSON.parse(saved);
        setHistory(parsed);
      }
    } catch (e) {
      console.error('Failed to load history', e);
    }
  };

  interface HistoryItem {
    date: string;
    data: {
      name: string; 
      value: number; // Price in $
      percentage: number;
      color: string;
      image: string;  // Image for coin
      symbol: string; //  Symbol of coin
    }[];
    totalValue: number;
  }

  const clearHistory = async () => {  // Clear AsyncStorage for cryptoDonutHistory
    try {
      await AsyncStorage.removeItem('cryptoDonutHistory');
      setHistory([]);
      return true;
    } catch (e) {
      console.error('Cleaning error:', e);
      return false;
    }
  };

  const addToHistory = async (newData: any[]) => {  // Added data in AsyncStorage
    const totalValue = newData.reduce((sum, item) => sum + item.value, 0);
    try {
      const newItem = { 
        date: new Date().toLocaleString(),
        data: newData,
        totalValue,
      };
      const updatedHistory = [newItem, ...history];
      await AsyncStorage.setItem('cryptoDonutHistory', JSON.stringify(updatedHistory));
      setHistory(updatedHistory);
    } catch (e) {
      console.error('Failed to save history', e);
    }
  };

  useEffect(() => {
    loadHistory();
  }, []);

  return { history, addToHistory, clearHistory };
};
Enter fullscreen mode Exit fullscreen mode

Localized Number Formatting

In the US, we write numbers like $324,654,765. But in many other countries, the comma is a decimal separator. Which means your users might read that as $324 and some cents. Not ideal.

To fix this, I formatted all numbers using the device’s locale — so users see values in the format they expect.

export const formatNumber = (value, { isCurrency = false, currency = 'USD', minimumFractionDigits = 0, maximumFractionDigits = 2 } = {}) => {
  if (value == null || isNaN(value)) return '';

  return value.toLocaleString(undefined, {
    style: isCurrency ? 'currency' : 'decimal',
    currency,
    minimumFractionDigits,
    maximumFractionDigits,
  });
};

// Also uses safeToFixed to safely round numbers.
export const safeToFixed = (value, decimals = 2) => {
  if (isNaN(value) || value == null) {
    // If the value is not a number or null, return a string with the default value
    return '0.00';
  }
  return value.toFixed(decimals);
}
Enter fullscreen mode Exit fullscreen mode

The Finishing Touches

With 4 hours left before midnight, I focused on the visuals. I found the tastiest-looking donut images on Vecteezy (free license), designed some Play Store slides in Corel Vector, and uploaded everything.

Mobile

I hit “Submit” just after midnight, watched the build go through, and went to bed thinking I was safe from the next inactivity purge for a while. Here’s the app on Google Play if you're curious.

But Then, Google Strikes Again

Next morning?

“Your developer account may be at risk due to inactivity.”

Come on, Google! I just published an app! I followed the rules!

In the Play Console, the message basically said:

  • Confirm your email and phone number (✅ done)
  • Publish a new app or release an update (✅ also done)

So yeah. Either they didn’t register my app as “active” yet, or I’ll need to wait and see if the red banner disappears. But hey — I now have:

  • An open-source project I’m proud of
  • A maybe-doomed dev account
  • And a ton of ideas for the next version

Thinking of adding a map with donut shops, built-in LLM chat, and even more data in the future. I'm planning how I'll implement new features:

I'm planning how I'll implement new features

Let’s Trade Stories

Got your own horror story about Google Play or dev accounts going rogue? Drop it in the comments. Share your thoughts, join the open-source repo — and if your story’s spicy enough, I owe you a donut.

I didn't understand anything, but it was very interesting.

GitHub repo: https://github.com/wladradchenko/doughfolio.wladradchenko.ru
Google Play: https://play.google.com/store/apps/details?id=com.wladradchenko.donut
Made with: ☕, 🍩, and 😅

Top comments (1)

Collapse
 
wladradchenko profile image
Wlad Radchenko • Edited

Hooray! The "Your developer account is inactive and may be closed" banner is finally gone.
I consider this a tiny but meaningful victory! 🎉

Some comments may only be visible to logged-in visitors. Sign in to view all comments.