TweetFollow Us on Twitter

Installer Plugins

Volume Number: 25
Issue Number: 06
Column Tag: Installers

Installer Plugins

Build a basic installer plug-in using Xcode

by José R.C. Cruz

Introduction

Up to now, you know two ways to customize an install session. For instance, to check if the target is the right one for the payload, you use a requirements script. To prepare the target to receive the payload, you use an action or an install script. Yet, there are cases where you may find these approaches inadequate. To handle those cases, you may need an installer plug-in.

This article will give you the background you will need to write your own installer plug-in. First, it explains how the plug-in fits into the basic install session. Then, it describes the plug-in API defined by the InstallerPlugin.framework. Next, it presents the plug-in template and its constituent files. Finally, it shows how to build a basic plug-in and test it in a basic package.

As always, you can get a copy of the featured projects from the MacTech website. Just go to the following URL to download your copy.:

ftp.mactech.com/src/mactech/volume25_2009/25.06.sit

Enter The Plug-In


Figure 1. Sequence of panels in a basic install session.

Several panels make up a basic install session (Figure 1). The Welcome panel appears when users start the session by double-clicking the package. The second panel, ReadMe, provides users with more information about the package's payload. The third one, License, shows the manufacturer's license terms governing the use of the payload. User can either reject these terms, thus ending the session, or accept them to continue. Next, the Select Destination panel lets users choose the target volume on which to install the payload. After that, the Custom Install panel allows users to select which payload to install on the target. Then the Standard Install panel gives users one last chance to change their minds before the actual install occurs. Finally, the Conclusion panel summarizes the results of the installation.

Not all of these panels need appear in an install session. For instance, the Readme and License panels (grey) are optional. The install session skips these panels if they have no text to display. The Select Destination and Custom Install panels (orange) are controlled by the package. They appear only if users are allowed to change the target volume or choice of payloads during the session.

Now suppose your package needs extra information from the users, and none of the panels are up to the task. For this case, you need to write an installer plug-in. A plug-in can insert a custom panel at specific points of the session (Figure 2). It resides in the directory Contents/Plugins of the package. Also, a plug-in has full access to the Cocoa framework. This allows the plug-in to do a wide variety of tasks, tasks that are either difficult or impossible to do with a script.

Yet, like any software technology, an installer plug-in has its limits and issues. For one, it cannot replace any of the basic install panels. A plug-in must always provide a panel -- paneless plug-ins are not supported. Also, you cannot debug a plug-in using Xcode's source debugger. Your only recourse is to have the plug-in send debug messages to the console.log file using either NSLog() or ASL (Apple System Log). Another issue is that only meta-packages and distribution packages support installer plug-ins. The new flat-file Leopard package does not support them at this time.

Last of all, at the time of writing, the plug-in API is still poorly documented. Your only options so far are to study the header files in the InstallerPlugin framework, and the sample plug-in project from Apple.


Figure 2. A plug-in in an install session

The Plug-in Framework

The InstallerPlugin framework serves as the basis of all installer plug-ins. This framework is located in /System/Library/Frameworks of the OS X boot volume. The framework comes with four header files, one of which, InstallerPlugins.h, is the main header. The other headers define three classes your plug-in can use. Figure 3 shows how these classes relate to each other.


Figure 3. The InstallerPlugin framework

To add this framework to your Xcode project, select the Frameworks and Libraries group on the Groups & Files pane of the editor window. Choose Add to Project from the Project menu, and use the Open File dialog to select the framework. Click the Add button to include the framework to the project. Then add the following line to your header file

   #import <InstallerPlugins/InstallerPlugins.h>

Let us now examine what each class has to offer.

The InstallerPane class

The InstallerPane class (Figure 4) handles the display of the custom panel. It also manages the interactions between the panel and the users. In short, this class serves as the controller for that panel.


Figure 4. The InstallerPane class

The class has six private outlets, four of which give the views that are linked to the class. For instance, the contentView outlet is the panel itself. The initialKeyView outlet is the first control widget that gets user focus after the panel is displayed. The firstKeyView outlet points to the current widget that gets any keyboard events, while the lastKeyView outlet points to the last widget to get any events.

The nextPane outlet returns the installer panel that follows the current one. And the outlet parentSection returns the InstallerSection instance for that panel (more on this later).

The InstallerPane class also comes with a wide range of messages. In this article, we will focus only on those messages that deal with panel behavior. There are eleven of these messages, which falls under two groups. The first group consists of delegate messages that the class gets at each panel event (Figure 5).


Figure 5. Panel events and delegate messages

Before the panel appears in the install session, it sends a willEnterPane message to the InstallerPane class. This allows the class to prepare the resources it needs to support the panel. Next, the panel appears and sends a title message to the class. The class responds with a localized NSString, which the panel displays near its upper-right corner (green). Then the panel sends a didEnterPane to the class. The latter can respond either by setting the default values on the panel or by starting the desired services.

When users click the Go Back or Continue button, the panel first sends a shouldExitPane to the InstallerPane class. If the class returns a NO, the panel remains active. On the other hand, if the class returns a YES, the panel sends a willExitPane back to the class. The class uses this moment to process the user data from the panel. The panel then disappears and sends a didExitPane back to the class. This is where the class can dispose the resources it used to support the panel.

The second group of messages allows the InstallerPane class to control some panel activity. For instance, to enable the Continue button, send a setNextEnabled message with a YES argument.

   [self setNextEnabled:YES];

To read the state of the Continue button, send a nextEnabled message. If the button is enabled, the message returns a YES; otherwise, it returns a NO.

   tFlg = [self nextEnabled];

To display the next panel, use the gotoNextPane message. For the previous panel, use the gotoPreviousPane message.

   tFlg = [self gotoNextPane];

These messages have the same effect as users clicking the Continue or Go Back buttons. Both return a YES if the desired panel appears without errors. On the other hand, if the panel does not exists or if an error occurs, both messages return a NO.

The InstallerSection class

Next is the InstallerSection class (Figure 6), which works as a controller for InstallerPane. It supplies the plug-in with data on the current install session. The InstallerPane class carries an instance of InstallerSection in its parentSection outlet. To access the instance, send a section message from InstallerPane.

   tSct = [self section];


Figure 6. The InstallerSection class

The InstallerSection class has one private outlet, firstPane. This outlet stores an instance of the InstallerPane class. By default, this is the same InstallerPane whose the custom panel appears during the install session. Since this outlet exists, it implies that a plug-in can have multiple instances of InstallerPane, each one with its own custom panel. We will explore this possibility in a future article.

Next, the InstallerSection class comes with ten methods, some of which you can override. This article, however, will focus only on those methods that a basic plug-in can use. For instance, to get the current panel, use the firstPane or activePane method. Either method will return the same InstallerPane instance if the plug-in has only one custom panel.

   tPnl = [[self section] firstPane];

To get the nib that carries the custom panel, send a bundle message to InstallerSection. This returns the bundle as an NSBundle object.

   tBndl = [[self section] bundle];

To read the title string for the active panel, call the title method. InstallerSection responds by returning the panel title as an NSString.

   tTitle = [[self section] title];

And to find out the current install state, use the state accessor. This gives you an instance of the InstallerState class, which is described next.

   tState = [[self section] state];

The InstallerState class

As stated earlier, the InstallerState class (Figure 7) returns the current state of the install session. Like InstallerSection, this class is instantiated by InstallerPane once the latter displays its panel. To access the instance, use the state accessor of InstallerSection.


Figure 7. The InstallerState class

There are four sets of methods in the InstallerState class. Each set corresponds to a specific stage in the install session. The first set returns the results of the License panel. To find out if users accepted the license terms, use the licenseAgreed method. The method returns a YES if users did accept the terms; otherwise, it returns a NO.

   tAgree = [[[self section] state] licenseAgreed];

To find out which localized license is displayed, use licenseAgreedLanguage. This method returns the language as an NSString.

   tAgree = [[[self section] state] licenseAgreedLanguage];

The second set of methods return the results of the Select Destination panel. They tell the plug-in where the package will install its payloads. To get the selected target volume, use the targetVolumePath method.

   tVol = [[[self section] state] targetVolumePath];

The method returns the mount point of the selected volume as an NSString. To get the final destination for the payload, call the targetPath method.

   tPth = [[[self section] state] targetPath];

This returns the destination's absolute path as an NSString. The returned path will also have the volume's mount point as part of its string.

The next set of methods give the results of the Custom Install panel. They tell the plug-in which payloads where chosen by the users. For a list of all payloads, call the method choiceDictionaries.

   tList  = [[[self section] state] choiceDictionaries];

The method returns its list as an NSArray. For a specific payload choice, use the method choiceDictionaryForIdentifier. Then pass the payload's choice ID as input.

   tPayload  = [[[self section] state] 
         choiceDictionaryForIdentifier:@"foobar"];

This returns the choice settings as an NSDictionary. There are three entries in this dictionary, each entry with its own unique key. The choice ID, for instance, uses InstallerState_Choice_Identifier as its key, the payload's destination path InstallerState_Choice_CustomLocation. And the choice state is under the key InstallerState_Choice_Installed. Figure 8 shows which field on the choice's Configuration panel corresponds to which key.


Figure 8. The choice states and their keys

The Plug-in Template

To create an installer plug-in, use the Xcode project template, aptly named, Installer Plugin. Xcode 3.0 files this template in the directory /Developer/Library/Xcode/Project Templates/Standard Apple Plug-ins. You can, of course, write your own plug-in project from scratch. Using the template, however, reduces the amount of guesswork on your part.

Figure 9 shows the bundles and files of the project template. Note that there are two project bundles: one with a .xcode suffix, the other .xcodeproj. Use the .xcodeproj bundle if your Xcode IDE is version 2.x or newer; use the .xcode bundle for older versions of Xcode.


Figure 9. The plug-in project template

Note also that there are only three items that you should be updating. The rest of the project items have default settings that will suffice for most cases.

The InstallerSection.plist file

This file defines where your custom panel will appear in the install session. This file must be placed in the Contents/Plugins directory of your installer package. Without this file, your package will ignore your installer plug-in.

Listing 1 shows the default contents of that file. Note the file list only six of the panels that appear in the install session. The first three on the list refer to the Welcome, Readme, and License panels. The Target entry is the Select Destination panel, and PackageSelection is either the Custom or Standard Install panel. Finally, the Install entry is the progress panel, which appears when the package starts installing its payloads.

Listing 1. The InstallerSection.plist file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>SectionOrder</key>
   <array>
      <string>Introduction</string>
      <string>ReadMe</string>
      <string>License</string>
      <string>«PROJECTNAME».bundle</string>
      <string>Target</string>
      <string>PackageSelection</string>
      <string>Install</string>      
   </array>
</dict>
</plist>

Note as well that the file places the plug-in between the License and Target entries. It also lists the plug-in under the generic name of PROJECTNAME. When you create your plug-in project using this template, Xcode replaces PROJECTNAME with your plug-in's name. If you move your plug-in's entry in the list, you change the position of its panel in the install session. Also, if you delete one of the entries in the list, you prevent that panel from appearing during the session. But be careful with this last step, as it may lead to unexpected results.

The InstallerPlugin.nib bundle

This bundle defines the look and feel of your plug-in's panel. Like all nib bundles, you use the Interface Builder to make changes to this bundle. Figure 10 shows the five default objects in this bundle. Two of the objects are for your plug-in's use. For instance, the View object is, obviously, an instance of NSView. This object carries the interface of your custom panel. Double-clicking it gives you an empty panel that is 418 pixels by 330 pixels in size. Do not, however, change the size of the panel. If you do, your interface widgets will appear misaligned during the install session.


Figure 10. The InstallerPlugin.nib bundle

The <<PROJECTNAMEASIDENTIFIER>>Pane is an instance of InstallerPane. Its name is set to the project's name after you create the project. For instance, if your plug-in project is Foobar, the InstallerPane object gets the name FoobarPane.

The other three objects are the usual proxy objects you find in most nib bundles. But in this nib, the File's Owner proxy refer to the instance of InstallerSection. Its one outlet, firstPane, is linked to the nib's InstallerPane instance. The Application proxy refers to the global NSApplication object, which will be the Installer utility for the plug-in. And the First Responder proxy refer to the first object in the responder chain. This proxy is currently not attached to any object on the nib.

The Pane files

Finally, there are the two files: Pane.h and Pane.m. These files define the InstallerPane controller for your panel. Again, when Xcode creates your plug-in project, it prefixes the project name to each file. Do not, however, change the assigned file names to something else. If you do, your plug-in will be unable to display its panel correctly.

Treat these files as you would any controller class files. For instance, you can add outlets and link them to specific widgets on the custom panel. You can define actions that your panel widgets can call at runtime. You can even add code to manipulate user data or pass that same data to your own custom model class.

CAVEAT

At the time of writing, the plug-in template has one interesting flaw. When you use it to create a project, the project's nib bundle loses all six outlets for its InstallerPane instance. Your plug-in will still work with this flaw present, but it will be unable to query the current install state.

To work around this flaw, save your nib bundle as NIB 2.x. With a text editor, open the file classes.nib that is inside your bundle. Then add the lines in Listing 2 to that file. These lines will restore the six outlets to your bundle. Save your changes when done.

Whether this flaw is fixed in Xcode 3.1 is unconfirmed.

Listing 2. Adding outlets to classes.nib

         <key>OUTLETS</key>
         <dict>
            <key>contentView</key>
            <string>NSView</string>
            <key>firstKeyView</key>
            <string>NSView</string>
            <key>initialKeyView</key>
            <string>NSView</string>
            <key>lastKeyView</key>
            <string>NSView</string>
            <key>nextPane</key>
            <string>InstallerPane</string>
            <key>parentSection</key>
            <string>id</string>
         </dict>

To Create A Plug-in

Let us now build a simple plug-in using the Installer Plug-in template. Our plug-in will ask users to enter their name, company, and product serial number during the install session. If the serial number is correct, the plug-in will enable the Continue button. Otherwise, it will display an alert dialog and disable the same button.

So, start up your copy of Xcode. Choose New Project from the File menu and pick Installer Plug-in from the list of project templates. Click the Continue button and set the project's name to Register. Leave the project's directory at the default location. Click the Finish button to create and open your new project.

Defining the panel

Select the entry RegisterPane.h from the Groups & Files pane. Add the following outlets to that file's @interface block.

   IBOutlet NSTextField   *oUser;
   IBOutlet NSTextField   *oComp;
   IBOutlet NSTextField   *oSerial;

Then add the following action to the same block.

   - (IBAction) registerCheck:(id)aSnd;

Next, double-click the Register.nib entry from the pane, thus opening the bundle in Interface Builder. From the Register window, select the icon «PROJECTNAMEASIDENTIFIER»Pane. Choose Identity Inspector from the Tools menu and set the Class field to RegisterPane. Scroll down to the Interface Builder Identity pane and set the Name field to RegisterPane. Save your changes by choosing Save from the File menu. To find out if your changes are correct, go to the Class Outlets pane of the palette. You should see your three outlets listed in that pane. Close the palette when you are done.

Now double-click the View icon on the Register window. Lay out the panel as shown in Figure 11. There are two sets of widgets on the panel. The first set consists of NSTextFields serving as static labels. The second set consists of NSTextFields serving as editable fields.


Figure 11. Layout of the custom panel

Select the RegisterPane icon and choose Connections Inspector from the Tools menu. With your pointing device, link each editable field to the right outlet (Figure 12, red). For this plug-in, the oUser outlet links to the User field, the oComp to the Company field, and oSerial to Serial. Then link all three editable fields to the registerCheck action (blue). Take care to leave the template's preset links alone. Save your changes and switch back to the Xcode editor.


Figure 12. Linking the panel widgets

Implementing the panel

We now enter the code-writing part of the project. Select the entry RegisterPane.m from the Groups & Files pane. Go to the delegate method didEnterPane and enter the code shown in Listing 3. This method sets each of the outlets to their default values. Then it disables the Continue button and enables the Go Back button.

Listing 3. Setting the default panel data

- (void)didEnterPane:(InstallerSectionDirection)aDir
{
      // initialize the following outlet fields
    [oUser setStringValue:@"your name goes here"];
    [oComp setStringValue:@"your company name"];
    [oSerial setStringValue:@"123456789"];
    
      // disable the Continue button
    [self setNextEnabled:NO];
    
      // enable the Go Back button
    [self setPreviousEnabled:YES];
}

Next, enter the code in Listing 4 to the delegate method shouldExitPane. First, the method reads the hash values from the oUser and oComp outlets. It then combines the two hash values and compares the results with the value from oSerial. If both values are different, the method displays a warning dialog to the users. It then disables the Continue button and returns a NO to prevent the panel change. On the other hand, if both values are the same, the method returns a YES to allow the change.

Listing 4. Checking the user data

- (BOOL)shouldExitPane:(InstallerSectionDirection)aDir
{
    NSUInteger tUsr, tCmp, tXor, tSN;
    NSAlert *tWrn;
    
   // check the direction of movement
    if (aDir == InstallerDirectionForward)
    {
        // read the hash values of each registration
        tUsr = [[oUser stringValue] hash];
        tCmp = [[oComp stringValue] hash];
        tXor = tUsr ^ tCmp;
        // read the serial number
        tSN = [oSerial intValue];
        if (tSN != tXor)
        {
            // create a warning dialog
            tWrn = [[NSAlert alloc] init];
            if (tWrn != nil)
            {
                // initialize the dialog
                [tWrn addButtonWithTitle:@"OK"];
                [tWrn setMessageText:@"Invalid serial number"];
                [tWrn setInformativeText:
            @"Please check and re-enter your registration information."];
                [tWrn setAlertStyle:NSInformationalAlertStyle];
                
                // display the warning dialog
                [tWrn runModal];
                
                // dispose the warning dialog
                [tWrn release];
            }
      // disable the Continue button
            [self setNextEnabled:NO];
            
            // prevent the panel movement
            return (NO);
        }
    }
// allow the panel movement
    return (YES);
}

Finally, to the registerCheck action, enter the code in Listing 5. This action checks the data in each outlet. If all outlets have a non-zero length string, the action then enables the Continue button. Otherwise, that same button remains disabled. Save your changes after you have updated these three methods.

Listing 5. Responding to the user entry

- (IBAction) registerCheck:(id)aSnd
{
    BOOL tChk;
    // check the registration fields
    tChk = ([[oUser stringValue] length] > 0);
    tChk &= ([[oComp stringValue] length] > 0);
    tChk &= ([[oSerial stringValue] length] > 0);
    
    // enable the Continue button
    [self setNextEnabled:tChk];
}

Now, a few words before we proceed to the next part of our project. First, note the use of the NSString hash function to generate the values for oUser and oComp. While this function is fine for our sample plug-in, it is impractical for real-world use. Future versions of NSString may behave differently. As a result, their hash functions may return a different value for the same string. A more reliable solution is for you to use your own hash algorithm.

Second, the plug-in only checks if the registration data is valid before it allows or deny product installation. This, again, is impractical because users can defeat the check by removing the plug-in. One good solution is to have the plug-in write its results to a hidden file. The installed payload can then look for this file and even read its data. If the data is correct, the payload behaves normally. If not, or if the file missing, the payload displays a reminder dialog or it runs in demo mode.

Building the plug-in

We are now ready to build our plug-in. But first, we must define where our plug-in's panel will appear in the install session. In this case, we want our panel to come after the Select Destination panel. Select the entry InstallerSection.plist from the Groups & Files pane. Locate the entry Register.bundle and move it after the entry for Target (Listing 6). Save your changs when done.

Listing 6. Modified contents of InstallerSection.plist

   <array>
      <string>Introduction</string>
      <string>ReadMe</string>
      <string>License</string>
      <string>Target</string>
      <string>Register.bundle</string>
      <string>PackageSelection</string>
      <string>Install</string>      
   </array>

Now build the plug-in by clicking the Build button on the Xcode toolbar. Xcode compiles each project file and links them to create the plug-in bundle. It then places the bundle, named Register.bundle, in the in the project subdirectory build/Release/.

Installing and testing the plug-in

To test the plug-in, you will need a basic installer package. Now when you prepare your installer project, make sure to set its Minimum Target to MacOS X 10.4. This will tell PackageMaker to use the distribution bundle as the package format.

In this article, we will use Foobar_Demo as our installer project. Its payload consists of three TIFF files, which will go into the directory /Users/Pictures. Build the package by choosing Build from the Project menu. When prompted, use Foobar as the package name. Go to the Finder and control-click the package to display its contextual menu. Choose Show Package Contents to open the bundle in a separate Finder window. Go to the Contents directory and create a new subdirectory named Plugins. Then copy the plug-in Register.bundle and the file InstallerSection.plist into that subdirectory (Figure 13). Close the Finder window when done.


Figure 13. Installing the plug-in and plist

Now double-click the Foobar package to start the install session. First, you get a modal dialog (Figure 14) warning you that the package will perform a custom task. This means the package recognizes your plug-in's presence. But it can also mean that the package contains an install action or script. Click Continue to proceed.


Figure 14. Warning the user

After the package displays its Welcome panel, you will see the name Register added to the list of panels (Figure 15, orange). You will also see that same name after Destination Select. This means the package knows that the plug-in has a custom panel, and it knows when to display the panel.


Figure 15. The Welcome panel with an updated list

Click the Continue button until you reach the Register panel. For the User field, enter the name "Alan Smithee" as the user. For the Company field, enter "Foobar". Leave the Serial field unchanged. You should see the Continue button enabled at this point. Now click that button. The package will display a warning dialog (Figure 16) telling you that the registration data is wrong.

Dismiss the dialog by clicking its OK button. Carefully type 1239302760 into the Serial field and then click the Continue button. This time, the package will display the next panel, which is the Custom Install panel.


Figure 16. Incorrect registration

Concluding Remarks

An installer plug-in is another way to customize your install session. With a plug-in, you can display a custom panel for other users to interact. You can use the Cocoa framework to do tasks that are hard, if not impossible, to do with an action or script.

Xcode comes with a basic template that you can base your plug-in project. This template sets the necessary files and nibs needed for such project. Adding a plug-in is as simple as a drag and drop. But keep in mind that only a meta-package and a distribution package will use plug-ins. The new flat-file package, introduced in 10.5, will ignore any plug-ins it may carry.

Recommended References

As stated earlier, Apple has yet to document the task of writing an installer plug-in. So far, your best options, besides this article, are to study their sample plug-in project, or the header files of the InstallerPlugin framework. You can also query the Installer-dev list archives for possible answer to your questions.

Stéphane Sudre, maker of Iceberg, also wrote a couple of pieces on the topic. Listed below are his online works for your benefit.

Stéphane Sudre. "Defining Installer Plugins". Iceberg Users Guide. Copyright 2008. Accessed on 2008 Aug 14. Online:

http://s.sudre.free.fr/Software/Iceberg.html

Stéphane Sudre. "Installer Plugins". Installation - The Lost Scrolls. 2008 Apr 10. Online: http://s.sudre.free.fr/Stuff/Installer/Installer_Plugins/index.html


JC is a freelance engineering writer from North Vancouver, British Columbia. He spends his time writing technical articles; tinkering with Cocoa, REALbasic, and Python; and visiting his foster nephew. He can be reached at anarakisware@gmail.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links... | Read more »
Price of Glory unleashes its 1.4 Alpha u...
As much as we all probably dislike Maths as a subject, we do have to hand it to geometry for giving us the good old Hexgrid, home of some of the best strategy games. One such example, Price of Glory, has dropped its 1.4 Alpha update, stocked full... | Read more »
The SLC 2025 kicks off this month to cro...
Ever since the Solo Leveling: Arise Championship 2025 was announced, I have been looking forward to it. The promotional clip they released a month or two back showed crowds going absolutely nuts for the previous competitions, so imagine the... | Read more »
Dive into some early Magicpunk fun as Cr...
Excellent news for fans of steampunk and magic; the Precursor Test for Magicpunk MMORPG Crystal of Atlan opens today. This rather fancy way of saying beta test will remain open until March 5th and is available for PC - boo - and Android devices -... | Read more »
Prepare to get your mind melted as Evang...
If you are a fan of sci-fi shooters and incredibly weird, mind-bending anime series, then you are in for a treat, as Goddess of Victory: Nikke is gearing up for its second collaboration with Evangelion. We were also treated to an upcoming... | Read more »
Square Enix gives with one hand and slap...
We have something of a mixed bag coming over from Square Enix HQ today. Two of their mobile games are revelling in life with new events keeping them alive, whilst another has been thrown onto the ever-growing discard pile Square is building. I... | Read more »
Let the world burn as you have some fest...
It is time to leave the world burning once again as you take a much-needed break from that whole “hero” lark and enjoy some celebrations in Genshin Impact. Version 5.4, Moonlight Amidst Dreams, will see you in Inazuma to attend the Mikawa Flower... | Read more »
Full Moon Over the Abyssal Sea lands on...
Aether Gazer has announced its latest major update, and it is one of the loveliest event names I have ever heard. Full Moon Over the Abyssal Sea is an amazing name, and it comes loaded with two side stories, a new S-grade Modifier, and some fancy... | Read more »
Open your own eatery for all the forest...
Very important question; when you read the title Zoo Restaurant, do you also immediately think of running a restaurant in which you cook Zoo animals as the course? I will just assume yes. Anyway, come June 23rd we will all be able to start up our... | Read more »
Crystal of Atlan opens registration for...
Nuverse was prominently featured in the last month for all the wrong reasons with the USA TikTok debacle, but now it is putting all that behind it and preparing for the Crystal of Atlan beta test. Taking place between February 18th and March 5th,... | Read more »

Price Scanner via MacPrices.net

AT&T is offering a 65% discount on the ne...
AT&T is offering the new iPhone 16e for up to 65% off their monthly finance fee with 36-months of service. No trade-in is required. Discount is applied via monthly bill credits over the 36 month... Read more
Use this code to get a free iPhone 13 at Visi...
For a limited time, use code SWEETDEAL to get a free 128GB iPhone 13 Visible, Verizon’s low-cost wireless cell service, Visible. Deal is valid when you purchase the Visible+ annual plan. Free... Read more
M4 Mac minis on sale for $50-$80 off MSRP at...
B&H Photo has M4 Mac minis in stock and on sale right now for $50 to $80 off Apple’s MSRP, each including free 1-2 day shipping to most US addresses: – M4 Mac mini (16GB/256GB): $549, $50 off... Read more
Buy an iPhone 16 at Boost Mobile and get one...
Boost Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering one year of free Unlimited service with the purchase of any iPhone 16. Purchase the iPhone at standard MSRP, and then choose... Read more
Get an iPhone 15 for only $299 at Boost Mobil...
Boost Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering the 128GB iPhone 15 for $299.99 including service with their Unlimited Premium plan (50GB of premium data, $60/month), or $20... Read more
Unreal Mobile is offering $100 off any new iP...
Unreal Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering a $100 discount on any new iPhone with service. This includes new iPhone 16 models as well as iPhone 15, 14, 13, and SE... Read more
Apple drops prices on clearance iPhone 14 mod...
With today’s introduction of the new iPhone 16e, Apple has discontinued the iPhone 14, 14 Pro, and SE. In response, Apple has dropped prices on unlocked, Certified Refurbished, iPhone 14 models to a... Read more
B&H has 16-inch M4 Max MacBook Pros on sa...
B&H Photo is offering a $360-$410 discount on new 16-inch MacBook Pros with M4 Max CPUs right now. B&H offers free 1-2 day shipping to most US addresses: – 16″ M4 Max MacBook Pro (36GB/1TB/... Read more
Amazon is offering a $100 discount on the M4...
Amazon has the M4 Pro Mac mini discounted $100 off MSRP right now. Shipping is free. Their price is the lowest currently available for this popular mini: – Mac mini M4 Pro (24GB/512GB): $1299, $100... Read more
B&H continues to offer $150-$220 discount...
B&H Photo has 14-inch M4 MacBook Pros on sale for $150-$220 off MSRP. B&H offers free 1-2 day shipping to most US addresses: – 14″ M4 MacBook Pro (16GB/512GB): $1449, $150 off MSRP – 14″ M4... Read more

Jobs Board

All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.