1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-02-07 04:10:08 +00:00

Added In-App Purchase sample code app

This commit is contained in:
hogliux 2017-08-10 12:05:46 +01:00
parent 3d379863f1
commit a092c19bcc
112 changed files with 18364 additions and 0 deletions

View file

@ -0,0 +1,435 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "../JuceLibraryCode/JuceHeader.h"
#include "VoicePurchases.h"
//==============================================================================
class InAppPurchaseApplication : public JUCEApplication,
private AsyncUpdater
{
public:
//==============================================================================
InAppPurchaseApplication() : voicePurchases (*this) {}
//==============================================================================
const String getApplicationName() override { return ProjectInfo::projectName; }
const String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return false; }
//==============================================================================
void initialise (const String&) override
{
Desktop::getInstance().getDefaultLookAndFeel().setUsingNativeAlertWindows (true);
dm.addAudioCallback (&player);
dm.initialiseWithDefaultDevices (0, 2);
mainWindow = new MainWindow;
Timer::callAfterDelay(1000, [] ()
{
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
"Your credit card will be charged!",
"You are running the sample code for JUCE In-App purchases."
"Although this is only sample code, it will still CHARGE YOUR CREDIT CARD!",
"Understood!");
});
}
void shutdown() override
{
mainWindow = nullptr;
dm.closeAudioDevice();
dm.removeAudioCallback (&player);
}
static InAppPurchaseApplication* getInstance()
{
return static_cast<InAppPurchaseApplication*> (JUCEApplication::getInstance());
}
//==============================================================================
SoundPlayer& getPlayer()
{
return player;
}
VoicePurchases& getPurchases()
{
return voicePurchases;
}
//==============================================================================
class MainContentComponent : public Component, private Button::Listener
{
public:
MainContentComponent()
{
setOpaque (true);
phraseListBox.setRowHeight (33);
phraseListBox.selectRow (0);
phraseListBox.updateContent();
voiceListBox.setRowHeight (66);
voiceListBox.selectRow (0);
voiceListBox.updateContent();
voiceListBox.getViewport()->setScrollOnDragEnabled (true);
addAndMakeVisible (phraseLabel);
addAndMakeVisible (phraseListBox);
addAndMakeVisible (playStopButton);
addAndMakeVisible (voiceLabel);
addAndMakeVisible (voiceListBox);
playStopButton.addListener (this);
#if JUCE_ANDROID || JUCE_IOS
auto screenBounds = Desktop::getInstance().getDisplays().getMainDisplay().userArea;
setSize (screenBounds.getWidth(), screenBounds.getHeight());
#else
setSize (800, 600);
#endif
}
void updateDisplay()
{
voiceListBox.updateContent();
voiceListBox.repaint();
}
private:
//==============================================================================
class PhraseModel : public ListBoxModel
{
public:
PhraseModel() {}
int getNumRows() override { return phrases.size(); }
void paintListBoxItem (int row, Graphics& g, int w, int h, bool isSelected) override
{
Rectangle<int> r (0, 0, w, h);
auto& lf = Desktop::getInstance().getDefaultLookAndFeel();
g.setColour (lf.findColour (isSelected ? TextEditor::highlightColourId : ListBox::backgroundColourId));
g.fillRect (r);
g.setColour (lf.findColour (ListBox::textColourId));
g.setFont (18);
String phrase = (isPositiveAndBelow (row, phrases.size()) ? phrases[row] : String{});
g.drawText (phrase, 10, 0, w, h, Justification::centredLeft);
}
private:
static StringArray phrases;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PhraseModel)
};
//==============================================================================
class VoiceModel : public ListBoxModel
{
public:
//==============================================================================
class VoiceRow : public Component, private Button::Listener, private Timer
{
public:
VoiceRow () : voices (getInstance()->getPurchases().getVoiceNames())
{
addAndMakeVisible (nameLabel);
addAndMakeVisible (purchaseButton);
addAndMakeVisible (priceLabel);
purchaseButton.addListener (this);
setSize (600, 33);
}
void paint (Graphics& g) override
{
auto r = getLocalBounds().reduced (4);
{
auto voiceIconBounds = r.removeFromLeft (r.getHeight());
g.setColour (Colours::black);
g.drawRect (voiceIconBounds);
voiceIconBounds.reduce (1, 1);
g.setColour (hasBeenPurchased ? Colours::white : Colours::grey);
g.fillRect (voiceIconBounds);
g.drawImage (avatar, voiceIconBounds.toFloat());
if (! hasBeenPurchased)
{
g.setColour (Colours::white.withAlpha (0.8f));
g.fillRect (voiceIconBounds);
if (purchaseInProgress)
getLookAndFeel().drawSpinningWaitAnimation (g, Colours::darkgrey,
voiceIconBounds.getX(), voiceIconBounds.getY(),
voiceIconBounds.getWidth(), voiceIconBounds.getHeight());
}
}
}
void resized() override
{
auto r = getLocalBounds().reduced (4 + 8, 4);
auto h = r.getHeight();
auto w = static_cast<int> (h * 1.5);
r.removeFromLeft (h);
purchaseButton.setBounds (r.removeFromRight (w).withSizeKeepingCentre (w, h / 2));
nameLabel.setBounds (r.removeFromTop (18));
priceLabel.setBounds (r.removeFromTop (18));
}
void update (int rowNumber, bool rowIsSelected)
{
isSelected = rowIsSelected;
rowSelected = rowNumber;
if (isPositiveAndBelow (rowNumber, voices.size()))
{
auto imageResourceName = voices[rowNumber] + "_png";
nameLabel.setText (voices[rowNumber], NotificationType::dontSendNotification);
auto purchase = getInstance()->getPurchases().getPurchase (rowNumber);
hasBeenPurchased = purchase.isPurchased;
purchaseInProgress = purchase.purchaseInProgress;
if (purchaseInProgress)
startTimer (1000 / 50);
else
stopTimer();
nameLabel.setFont (Font (16).withStyle (Font::bold | (hasBeenPurchased ? 0 : Font::italic)));
nameLabel.setColour (Label::textColourId, hasBeenPurchased ? Colours::white : Colours::grey);
priceLabel.setFont (Font (10).withStyle (purchase.priceIsKnown ? 0 : Font::italic));
priceLabel.setColour (Label::textColourId, hasBeenPurchased ? Colours::white : Colours::grey);
priceLabel.setText (purchase.purchasePrice, NotificationType::dontSendNotification);
if (rowNumber == 0)
{
purchaseButton.setButtonText ("Internal");
purchaseButton.setEnabled (false);
}
else
{
purchaseButton.setButtonText (hasBeenPurchased ? "Purchased" : "Purchase");
purchaseButton.setEnabled (! hasBeenPurchased && purchase.priceIsKnown);
}
setInterceptsMouseClicks (! hasBeenPurchased, ! hasBeenPurchased);
int rawSize;
if (auto* rawData = BinaryData::getNamedResource (imageResourceName.toRawUTF8(), rawSize))
{
MemoryInputStream imageData (rawData, static_cast<size_t> (rawSize), false);
avatar = PNGImageFormat().decodeImage (imageData);
}
}
}
private:
//==============================================================================
void buttonClicked (Button*) override
{
if (rowSelected >= 0)
{
if (! hasBeenPurchased)
{
getInstance()->getPurchases().purchaseVoice (rowSelected);
purchaseInProgress = true;
startTimer (1000 / 50);
}
}
}
void timerCallback() override { repaint(); }
//==============================================================================
bool isSelected = false, hasBeenPurchased = false, purchaseInProgress = false;
int rowSelected = -1;
Image avatar;
StringArray voices;
Label nameLabel, priceLabel;
TextButton purchaseButton {"Purchase"};
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoiceRow)
};
//==============================================================================
VoiceModel() : voiceProducts (getInstance()->getPurchases().getVoiceNames()) {}
int getNumRows() override { return voiceProducts.size(); }
Component* refreshComponentForRow (int row, bool selected, Component* existing) override
{
if (isPositiveAndBelow (row, voiceProducts.size()))
{
if (existing == nullptr)
existing = new VoiceRow;
if (auto* voiceRow = dynamic_cast<VoiceRow*> (existing))
voiceRow->update (row, selected);
return existing;
}
return nullptr;
}
void paintListBoxItem (int, Graphics& g, int w, int h, bool isSelected) override
{
auto r = Rectangle<int> (0, 0, w, h).reduced (4);
auto& lf = Desktop::getInstance().getDefaultLookAndFeel();
g.setColour (lf.findColour (isSelected ? TextEditor::highlightColourId : ListBox::backgroundColourId));
g.fillRect (r);
}
private:
StringArray voiceProducts;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoiceModel)
};
//==============================================================================
void resized() override
{
auto r = getLocalBounds().reduced (20);
{
auto phraseArea = r.removeFromTop (r.getHeight() / 2);
phraseLabel.setBounds (phraseArea.removeFromTop (36).reduced (0, 10));
playStopButton.setBounds (phraseArea.removeFromBottom (50).reduced (0, 10));
phraseListBox.setBounds (phraseArea);
}
{
auto voiceArea = r;
voiceLabel.setBounds (voiceArea.removeFromTop (36).reduced (0, 10));
voiceListBox.setBounds (voiceArea);
}
}
void paint (Graphics& g) override
{
g.fillAll (Desktop::getInstance().getDefaultLookAndFeel()
.findColour (ResizableWindow::backgroundColourId));
}
//==============================================================================
void buttonClicked (Button*) override
{
MemoryOutputStream resourceName;
auto idx = voiceListBox.getSelectedRow();
if (isPositiveAndBelow (idx, soundNames.size()))
{
resourceName << soundNames[idx] << phraseListBox.getSelectedRow() << "_ogg";
int numBytes;
if (auto* data = BinaryData::getNamedResource (resourceName.toString().toRawUTF8(), numBytes))
getInstance()->getPlayer().play (data, static_cast<size_t> (numBytes));
}
}
//==============================================================================
StringArray soundNames = getInstance()->getPurchases().getVoiceNames();
PhraseModel phraseModel;
Label phraseLabel {"phraseLabel", NEEDS_TRANS ("Phrases:")};
ListBox phraseListBox { "phraseListBox", &phraseModel };
TextButton playStopButton {"Play"};
VoiceModel voiceModel;
Label voiceLabel {"voiceLabel", NEEDS_TRANS ("Voices:")};
ListBox voiceListBox { "voiceListBox", &voiceModel };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
//==============================================================================
class MainWindow : public DocumentWindow
{
public:
MainWindow () : DocumentWindow (ProjectInfo::projectName,
Desktop::getInstance().getDefaultLookAndFeel()
.findColour (ResizableWindow::backgroundColourId),
DocumentWindow::allButtons)
{
setUsingNativeTitleBar (true);
setContentOwned (new MainContentComponent(), true);
#if JUCE_ANDROID || JUCE_IOS
setFullScreen (true);
#else
centreWithSize (getWidth(), getHeight());
#endif
setVisible (true);
}
void closeButtonPressed() override
{
JUCEApplication::getInstance()->systemRequestedQuit();
}
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
};
private:
//==============================================================================
void handleAsyncUpdate() override
{
if (auto* contentComponent = dynamic_cast<MainContentComponent*> (mainWindow->getContentComponent()))
contentComponent->updateDisplay();
}
//==============================================================================
VoicePurchases voicePurchases;
AudioDeviceManager dm;
SoundPlayer player;
ScopedPointer<MainWindow> mainWindow;
ScopedPointer<AlertWindow> alertWindow;
};
StringArray InAppPurchaseApplication::MainContentComponent::PhraseModel::phrases {{"I love JUCE!", "The five dimensions of touch", "Make it fast!"}};
//==============================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (InAppPurchaseApplication)

View file

@ -0,0 +1,194 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
//==============================================================================
class VoicePurchases : private InAppPurchases::Listener
{
public:
//==============================================================================
struct VoiceProduct
{
const char* identifier;
const char* humanReadable;
bool isPurchased, priceIsKnown, purchaseInProgress;
String purchasePrice;
};
//==============================================================================
VoicePurchases (AsyncUpdater& asyncUpdater)
: guiUpdater (asyncUpdater)
{
voiceProducts = Array<VoiceProduct>(
{VoiceProduct {"robot", "Robot", true, true, false, "Free" },
VoiceProduct {"jules", "Jules", false, false, false, "Retrieving price..." },
VoiceProduct {"fabian", "Fabian", false, false, false, "Retrieving price..." },
VoiceProduct {"ed", "Ed", false, false, false, "Retrieving price..." },
VoiceProduct {"lukasz", "Lukasz", false, false, false, "Retrieving price..." },
VoiceProduct {"jb", "JB", false, false, false, "Retrieving price..." } });
}
~VoicePurchases()
{
inAppPurchases.removeListener (this);
}
//==============================================================================
VoiceProduct getPurchase (int voiceIndex)
{
if (! havePurchasesBeenRestored)
{
havePurchasesBeenRestored = true;
inAppPurchases.addListener (this);
inAppPurchases.restoreProductsBoughtList (true);
}
return voiceProducts[voiceIndex];
}
void purchaseVoice (int voiceIndex)
{
if (havePricesBeenFetched && isPositiveAndBelow (voiceIndex, voiceProducts.size()))
{
auto& product = voiceProducts.getReference (voiceIndex);
if (! product.isPurchased)
{
product.purchaseInProgress = true;
inAppPurchases.purchaseProduct (product.identifier, false);
}
}
}
StringArray getVoiceNames() const
{
StringArray names;
for (auto& voiceProduct : voiceProducts)
names.add (voiceProduct.humanReadable);
return names;
}
private:
//==============================================================================
void productsInfoReturned (const Array<InAppPurchases::Product>& products) override
{
if (products.size() != 0)
{
for (auto product : products)
{
auto idx = findVoiceIndexFromIdentifier (product.identifier);
if (isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.priceIsKnown = true;
voiceProduct.purchasePrice = product.price;
}
}
}
else if (! inAppPurchases.isInAppPurchasesSupported())
{
for (auto idx = 1; idx < voiceProducts.size(); ++idx)
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.isPurchased = false;
voiceProduct.priceIsKnown = false;
voiceProduct.purchasePrice = "In-App purcahses unavailable";
}
}
guiUpdater.triggerAsyncUpdate();
}
void productPurchaseFinished (const PurchaseInfo& info, bool success, const String&) override
{
auto idx = findVoiceIndexFromIdentifier (info.purchase.productId);
if (isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.isPurchased = success;
voiceProduct.purchaseInProgress = false;
guiUpdater.triggerAsyncUpdate();
}
}
void purchasesListRestored (const Array<PurchaseInfo>& infos, bool success, const String&) override
{
if (success)
{
for (auto& info : infos)
{
auto idx = findVoiceIndexFromIdentifier (info.purchase.productId);
if (isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.isPurchased = true;
}
}
guiUpdater.triggerAsyncUpdate();
}
if (! havePricesBeenFetched)
{
havePricesBeenFetched = true;
StringArray identifiers;
for (auto& voiceProduct : voiceProducts)
identifiers.add (voiceProduct.identifier);
inAppPurchases.getProductsInformation(identifiers);
}
}
//==============================================================================
int findVoiceIndexFromIdentifier (String identifier) const
{
identifier = identifier.toLowerCase();
for (auto i = 0; i < voiceProducts.size(); ++i)
if (String (voiceProducts.getReference (i).identifier) == identifier)
return i;
return -1;
}
//==============================================================================
AsyncUpdater& guiUpdater;
bool havePurchasesBeenRestored = false, havePricesBeenFetched = false;
InAppPurchases inAppPurchases;
Array<VoiceProduct> voiceProducts;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases)
};